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 763c6aaec0 Fix deferred code bugs surfaced during Javadoc audits 
(TODO-270)
763c6aaec0 is described below

commit 763c6aaec090d78415f009af096c6e2e11fca97d
Author: James Bognar <[email protected]>
AuthorDate: Mon Jul 20 10:50:58 2026 -0400

    Fix deferred code bugs surfaced during Javadoc audits (TODO-270)
    
    Category A (clear bugs): 3-arg serialize-only swap() overloads no longer
    always-throw; prototext/toml tokenizer printf-injection hardening;
    JsonSchema.getAdditionalProperties() reads the correct backing field;
    Config import statements implemented (ConfigMap.setImport/removeImport) +
    Config.setImport honors its sectionName param; SwaggerException single
    format pass; FluentRequestHeaderAssertion propagates its creator;
    CookieList null-coalesces null getCookies().
    
    Category B (per maintainer resolutions): null-guard null-hostile lookup
    getters (RrpcInterfaceMethodMeta, DebugConfig, BeanRegistry, ClassMeta.same,
    ResourceDescription hashCode/compareTo); null-coalesce addDefault(...) and
    ContentSecurityPolicy.directive varargs; set/get null-contract parity
    (HttpPartSchema enum setters, Iso8601Utils, McpServerConfig, 
RemoteOperationArg);
    refresh stale Javadoc examples (AtomBuilder, MenuItemWidget, 
BasicDebugEnablement);
    honor previously-ignored extension params 
(MarshallingTraverseSession.onError,
    Json5List.ofString, MarshallingSession.toBeanMap PropertyNamer).
    
    Co-authored-by: Cursor <[email protected]>
---
 .../org/apache/juneau/bean/atom/AtomBuilder.java   |  8 +-
 .../apache/juneau/bean/jsonschema/JsonSchema.java  |  2 +-
 .../juneau/bean/jsonschema/JsonSchema_Test.java    |  9 ++
 .../org/apache/juneau/commons/bean/BeanMeta.java   | 66 +++++++++++++--
 .../main/java/org/apache/juneau/config/Config.java |  4 +-
 .../apache/juneau/config/internal/ConfigMap.java   | 37 +++++++--
 .../java/org/apache/juneau/config/Config_Test.java | 39 +++++++--
 .../org/apache/juneau/marshall/BeanRegistry.java   |  4 +-
 .../java/org/apache/juneau/marshall/ClassMeta.java | 25 +++++-
 .../apache/juneau/marshall/MarshallingContext.java |  6 +-
 .../apache/juneau/marshall/MarshallingSession.java | 33 ++++++--
 .../marshall/MarshallingTraverseSession.java       |  7 +-
 .../juneau/marshall/httppart/HttpPartSchema.java   | 12 ++-
 .../apache/juneau/marshall/json5/Json5List.java    |  5 +-
 .../marshall/prototext/PrototextTokenizer.java     |  3 +-
 .../apache/juneau/marshall/toml/TomlTokenizer.java | 13 +--
 .../apache/juneau/marshall/utils/Iso8601Utils.java | 18 ++--
 .../apache/juneau/marshall/BeanRegistry_Test.java  | 28 +++++--
 .../org/apache/juneau/marshall/ClassMeta_Test.java | 11 +++
 .../juneau/marshall/MarshallingContext_Test.java   | 22 +++++
 .../juneau/marshall/MarshallingSession_Test.java   | 97 ++++++++++++++++++++++
 .../httppart/HttpPartSchema_Validation_Test.java   | 18 ++++
 .../juneau/marshall/json5/Json5List_Test.java      | 13 +++
 .../prototext/PrototextTokenizer_Test.java         | 18 ++++
 .../juneau/marshall/toml/TomlTokenizer_Test.java   | 18 ++++
 .../juneau/marshall/utils/Iso8601Utils_Test.java   |  8 ++
 .../server/beans/ResourceDescription_Test.java     | 19 +++++
 .../juneau/rest/server/debug/DebugConfig_Test.java |  2 +
 .../server/httppart/RequestAttributes_Test.java    | 13 +++
 .../httppart/RequestQueryParamList_Test.java       | 14 ++++
 .../client/classic/remote/RemoteOperationArg.java  |  2 +-
 .../juneau/http/header/ContentSecurityPolicy.java  |  4 +-
 .../http/remote/RrpcInterfaceMethodMeta.java       |  8 +-
 .../http/header/ContentSecurityPolicy_Test.java    |  7 ++
 .../http/remote/RrpcInterfaceMethodMeta_Test.java  | 24 ++++--
 .../juneau/rest/server/mcp/McpServerConfig.java    |  4 +-
 .../apache/juneau/rest/server/arg/CookieList.java  | 10 ++-
 .../assertions/FluentRequestHeaderAssertion.java   |  2 +-
 .../rest/server/beans/ResourceDescription.java     |  6 +-
 .../rest/server/debug/BasicDebugEnablement.java    |  2 +-
 .../juneau/rest/server/debug/DebugConfig.java      |  2 +-
 .../rest/server/httppart/RequestAttributes.java    |  4 +
 .../rest/server/httppart/RequestFormParamList.java |  2 +
 .../rest/server/httppart/RequestPathParamList.java |  2 +
 .../server/httppart/RequestQueryParamList.java     |  2 +
 .../rest/server/swagger/SwaggerException.java      |  6 +-
 .../juneau/rest/server/widget/MenuItemWidget.java  |  6 +-
 .../juneau/rest/server/arg/CookieList_Test.java    | 48 +++++++++++
 .../FluentRequestHeaderAssertion_Test.java         | 62 ++++++++++++++
 .../rest/server/swagger/SwaggerException_Test.java | 51 ++++++++++++
 50 files changed, 730 insertions(+), 96 deletions(-)

diff --git 
a/juneau-bean/juneau-bean-atom/src/main/java/org/apache/juneau/bean/atom/AtomBuilder.java
 
b/juneau-bean/juneau-bean-atom/src/main/java/org/apache/juneau/bean/atom/AtomBuilder.java
index a9fc7f9825..8cfd29065b 100644
--- 
a/juneau-bean/juneau-bean-atom/src/main/java/org/apache/juneau/bean/atom/AtomBuilder.java
+++ 
b/juneau-bean/juneau-bean-atom/src/main/java/org/apache/juneau/bean/atom/AtomBuilder.java
@@ -33,11 +33,11 @@ import org.apache.juneau.marshall.*;
  *     Feed <jv>feed</jv> =
  *             <jsm>feed</jsm>(<js>"tag:juneau.sample.com,2013:1"</js>, 
<js>"Juneau ATOM specification"</js>,
  *                     <js>"2013-05-08T12:29:29Z"</js>)
- *             .subtitle(<jsm>text</jsm>(<js>"html"</js>)
- *                     .children(<js>"A &lt;em&gt;lot&lt;/em&gt; of effort 
went into making this effortless"</js>))
- *             .links(
+ *             .setSubtitle(<jsm>text</jsm>(<js>"html"</js>)
+ *                     .setText(<js>"A &lt;em&gt;lot&lt;/em&gt; of effort went 
into making this effortless"</js>))
+ *             .setLinks(
  *                     <jsm>link</jsm>(<js>"alternate"</js>, 
<js>"text/html"</js>, <js>"http://www.sample.com/";</js>)
- *                             .hreflang(<js>"en"</js>),
+ *                             .setHreflang(<js>"en"</js>),
  *                     <jsm>link</jsm>(<js>"self"</js>, 
<js>"application/atom+xml"</js>, <js>"http://www.sample.com/feed.atom";</js>)
  *             );
  * </p>
diff --git 
a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchema.java
 
b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchema.java
index 9631b3e799..c8362ed3ab 100644
--- 
a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchema.java
+++ 
b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchema.java
@@ -737,7 +737,7 @@ public class JsonSchema {
        @Swap(BooleanOrSchemaSwap.class)
        public Object getAdditionalProperties() {
                if (nn(additionalPropertiesBoolean))
-                       return additionalItemsBoolean;
+                       return additionalPropertiesBoolean;
                return additionalPropertiesSchema;
        }
 
diff --git 
a/juneau-bean/juneau-bean-jsonschema/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchema_Test.java
 
b/juneau-bean/juneau-bean-jsonschema/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchema_Test.java
index d349e40a67..7c600b3f4f 100644
--- 
a/juneau-bean/juneau-bean-jsonschema/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchema_Test.java
+++ 
b/juneau-bean/juneau-bean-jsonschema/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchema_Test.java
@@ -477,6 +477,15 @@ public class JsonSchema_Test extends TestBase {
                assertThrows(RuntimeException.class, () -> 
x.setAdditionalProperties("invalid"));
        }
 
+       @Test void b23e2_getAdditionalPropertiesAsBooleanFromGetter() {
+               // Regression: getAdditionalProperties() used to return the 
additionalItems backing field, so a
+               // boolean additionalProperties could never be read back and 
could be masked by additionalItems.
+               var x = new JsonSchema()
+                       .setAdditionalItems(Boolean.FALSE)
+                       .setAdditionalProperties(Boolean.TRUE);
+               assertEquals(Boolean.TRUE, x.getAdditionalProperties());
+       }
+
        @Test void b23f_setItems_jsonSchemaArray_path() {
                var x = new JsonSchema().setItems(new JsonSchemaArray(new 
JsonSchemaRef("http://a";)));
                assertNotNull(x.getItemsAsSchemaArray());
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMeta.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMeta.java
index 6b33fbba58..bbc188e8dd 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMeta.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMeta.java
@@ -211,6 +211,26 @@ public class BeanMeta<T> {
         * @return A {@link BeanMetaValue} containing the bean metadata (if 
successful) or a reason why it's not a bean.
         */
        public static <T> BeanMetaValue<T> create(BeanInfo<T> cm, ClassInfo 
implClass) {
+               return create(cm, implClass, null);
+       }
+
+       /**
+        * Same as {@link #create(BeanInfo, ClassInfo)} but allows an explicit 
{@link PropertyNamer} to override the one
+        * that would otherwise be derived from the bean filter or {@link 
BeanConfigContext#getPropertyNamer()}.
+        *
+        * <p>
+        * The returned {@link BeanMeta} is <b>not</b> cached anywhere — 
callers that need a namer-specific view (for
+        * example, {@code MarshallingSession#toBeanMap(Object, 
PropertyNamer)}) build one on demand so the shared,
+        * {@code Class}-keyed default {@link BeanMeta} cache is never polluted 
with alternate-namer property keys.
+        *
+        * @param <T> The class type.
+        * @param cm The bean type info for the class to create bean metadata 
for.  Must not be <jk>null</jk>.
+        * @param implClass Optional implementation class info to use when 
looking for a no-arg constructor.  Can be <jk>null</jk>.
+        * @param propertyNamer Optional property namer to use for deriving 
property names.  If <jk>null</jk>, the namer is
+        *      resolved the normal way (bean filter, then {@link 
BeanConfigContext#getPropertyNamer()}).
+        * @return A {@link BeanMetaValue} containing the bean metadata (if 
successful) or a reason why it's not a bean.
+        */
+       public static <T> BeanMetaValue<T> create(BeanInfo<T> cm, ClassInfo 
implClass, PropertyNamer propertyNamer) {
                try {
                        var cfg = cm.getBeanConfigContext();
                        var ap = cfg.getAnnotationProvider();
@@ -229,7 +249,7 @@ public class BeanMeta<T> {
                if ((! 
cfg.getBeanClassVisibility().isVisible(cm.getModifiers()) || 
cm.isAnonymousClass()) && ! bmi.hasBeanRegistrationAnnotation(cfg, cm))
                        return notABean("Class is not public");
 
-                       var bm = new BeanMeta<>(cm, bmi.buildBeanFilter(cm), 
null, implClass);
+                       var bm = new BeanMeta<>(cm, bmi.buildBeanFilter(cm), 
null, implClass, propertyNamer);
 
                        if (nn(bm.notABeanReason))
                                return notABean(bm.notABeanReason);
@@ -240,6 +260,21 @@ public class BeanMeta<T> {
                }
        }
 
+       /*
+        * Returns the property namer that should be used to derive property 
names for this bean.
+        *
+        * <p>
+        * Precedence: an explicit per-instance {@link #propertyNamerOverride} 
(used by the
+        * {@code toBeanMap(Object, PropertyNamer)} path) wins over everything; 
otherwise the bean filter's namer, then the
+        * {@link BeanConfigContext#getPropertyNamer() configured default}.  
Preserving this fallback order keeps the
+        * default-cache construction path behaviorally identical to before the 
override was introduced.
+        */
+       private PropertyNamer effectivePropertyNamer() {
+               if (nn(propertyNamerOverride))
+                       return propertyNamerOverride;
+               return o(beanFilter).map(x -> 
x.getPropertyNamer()).orElse(config.getPropertyNamer());
+       }
+
        /*
         * Extracts the property name from {@link BeanProp @BeanProp} or {@link 
Name @Name} annotations.
         *
@@ -335,6 +370,7 @@ public class BeanMeta<T> {
        private final Map<String,BeanPropertyMeta> properties;                  
   // The properties on the target class.
        private final Map<Method,String> setterProps;                           
   // The setter properties on the target class.
        private final boolean unsortedProperties;                               
   // Whether properties should use natural JVM-dependent order.
+       private final PropertyNamer propertyNamerOverride;                      
   // Optional per-instance namer override.  Null means resolve the normal way 
(bean filter, then config).  Non-null takes precedence over both.
        private final ClassInfo stopClass;                                      
   // The stop class for hierarchy traversal.
        private final BeanPropertyMeta typeProperty;                            
   // "_type" mock bean property.
        private final String typePropertyName;                                  
   // "_type" property actual name.
@@ -400,7 +436,22 @@ public class BeanMeta<T> {
         * @param implClass Optional implementation class constructor to use if 
one cannot be found. Can be <jk>null</jk>.
         */
        protected BeanMeta(BeanInfo<T> cm, BeanFilter bf, String[] pNames, 
ClassInfo implClass) {
-               this(cm, cm, cm.getBeanConfigContext(), 
cm.getMarshallingContext(), bf, pNames, implClass);
+               this(cm, cm, cm.getBeanConfigContext(), 
cm.getMarshallingContext(), bf, pNames, implClass, null);
+       }
+
+       /**
+        * Same as {@link #BeanMeta(BeanInfo, BeanFilter, String[], ClassInfo)} 
but allows an explicit
+        * {@link PropertyNamer} override.
+        *
+        * @param cm The class metadata for the bean class.  Must not be 
<jk>null</jk>.
+        * @param bf Optional bean filter to apply. Can be <jk>null</jk>.
+        * @param pNames Explicit list of property names and order. If 
<jk>null</jk>, properties are determined automatically.
+        * @param implClass Optional implementation class constructor to use if 
one cannot be found. Can be <jk>null</jk>.
+        * @param propertyNamerOverride Optional namer that, when 
non-<jk>null</jk>, takes precedence over the bean filter
+        *      and {@link BeanConfigContext#getPropertyNamer()} for deriving 
property names.
+        */
+       protected BeanMeta(BeanInfo<T> cm, BeanFilter bf, String[] pNames, 
ClassInfo implClass, PropertyNamer propertyNamerOverride) {
+               this(cm, cm, cm.getBeanConfigContext(), 
cm.getMarshallingContext(), bf, pNames, implClass, propertyNamerOverride);
        }
 
        /**
@@ -416,19 +467,20 @@ public class BeanMeta<T> {
         * @param config The bean-modeling configuration.  Must not be 
<jk>null</jk>.
         */
        protected BeanMeta(Class<T> beanClass, BeanConfigContext config) {
-               this(null, info(assertArgNotNull("beanClass", beanClass)), 
assertArgNotNull("config", config), null, null, null, null);
+               this(null, info(assertArgNotNull("beanClass", beanClass)), 
assertArgNotNull("config", config), null, null, null, null, null);
        }
 
        @SuppressWarnings({
                "java:S3776", // Cognitive complexity acceptable for bean 
metadata initialization
-               "java:S107"   // 7 parameters needed to support both 
construction paths
+               "java:S107"   // 8 parameters needed to support both 
construction paths plus the optional namer override
        })
-       private BeanMeta(BeanInfo<T> cm, ClassInfo ci0, BeanConfigContext 
config, Object mc, BeanFilter bf, String[] pNames, ClassInfo implClass) {
+       private BeanMeta(BeanInfo<T> cm, ClassInfo ci0, BeanConfigContext 
config, Object mc, BeanFilter bf, String[] pNames, ClassInfo implClass, 
PropertyNamer propertyNamerOverride) {
                classMeta = cm;
                classInfo = ci0;
                this.config = config;
                marshallingContext = mc;
                beanFilter = bf;
+               this.propertyNamerOverride = propertyNamerOverride;
                implClassConstructor = o(implClass).map(x -> 
x.getPublicConstructor(x2 -> x2.hasNumParameters(0)).orElse(null)).orElse(null);
                fluentSetters = config.isFindFluentSetters() || (nn(bf) && 
bf.isFluentSetters());
                stopClass = o(bf).map(x -> 
x.getStopClass()).orElse(info(Object.class));
@@ -449,7 +501,7 @@ public class BeanMeta<T> {
                var propertyBeanRegistriesTemp = 
CollectionUtils.<BeanPropertyMeta,BeanRegistryLookup>map();  // Per-property 
BeanRegistry side-map.
                var unsortedPropertiesTemp = false;
                var btList = ap.find(BeanType.class, classInfo);
-               var propertyNamer = o(bf).map(x -> 
x.getPropertyNamer()).orElse(config.getPropertyNamer());
+               var propertyNamer = effectivePropertyNamer();
 
                // resolveTypePropertyName may return null on the commons-side 
NOOP path; fall back to the configured default.
                var resolvedTypePropertyName = 
config.getBeanMetaInitializer().resolveTypePropertyName(config, classInfo);
@@ -1295,7 +1347,7 @@ public class BeanMeta<T> {
                var ap = config.getAnnotationProvider();
                var ci = classInfo;
                var v = config.getBeanMethodVisibility();
-               var pn = o(beanFilter).map(x -> 
x.getPropertyNamer()).orElse(config.getPropertyNamer());
+               var pn = effectivePropertyNamer();
                var suppressedFromBeanIgnoredFields = 
findSuppressedPropertyNamesFromIgnoredFields(pn);
 
                classHierarchy.get().forEach(c2 -> {
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
index a4dadb9fcb..d89cb20ba7 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
@@ -944,7 +944,7 @@ public class Config extends Context implements 
ConfigEventListener {
         */
        public Config removeImport(String sectionName, String importName) {
                checkWrite();
-               configMap.removeImport(sectionName, importName);
+               configMap.removeImport(section(sectionName), importName);
                return this;
        }
 
@@ -1120,7 +1120,7 @@ public class Config extends Context implements 
ConfigEventListener {
         */
        public Config setImport(String sectionName, String importName, 
List<String> preLines) {
                checkWrite();
-               configMap.setImport(section(name), importName, preLines);
+               configMap.setImport(section(sectionName), importName, preLines);
                return this;
        }
 
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
index d9fdb3c4b9..cb6b9eda7f 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
@@ -550,20 +550,30 @@ public class ConfigMap implements ConfigStoreListener {
        }
 
        /**
-        * Not implemented.
+        * Removes the import statement with the specified name.
+        *
+        * <p>
+        * The imported config is detached from this map: its entries are no 
longer visible through this map and its
+        * change-propagation listeners are unregistered.  No-op if no matching 
import exists.
         *
         * @param section
-        *      The section name where to place the import statement.
+        *      The section name where the import statement was placed.
         *      <br>Must not be <jk>null</jk>.
         *      <br>Use blank for the default section.
         * @param importName
         *      The import name.
         *      <br>Must not be <jk>null</jk>.
         * @return This object.
-        * @throws UnsupportedOperationException Always - this operation is not 
implemented.
         */
        public ConfigMap removeImport(String section, String importName) {
-               throw uoex();
+               checkSectionName(section);
+               try (var x = lock.write()) {
+                       imports.stream().filter(y -> 
y.getConfigName().equals(importName)).findFirst().ifPresent(y -> {
+                               y.unregisterAll();
+                               imports.remove(y);
+                       });
+               }
+               return this;
        }
 
        /**
@@ -631,7 +641,12 @@ public class ConfigMap implements ConfigStoreListener {
        }
 
        /**
-        * Not implemented.
+        * Creates the specified import statement if it doesn't already exist.
+        *
+        * <p>
+        * The named config is resolved through this map's {@link ConfigStore 
store} and attached to this map so that its
+        * entries become visible through this map and subsequent changes to it 
are propagated to this map's listeners.
+        * No-op if an import with the same name already exists.
         *
         * @param section
         *      The section name where to place the import statement.
@@ -644,10 +659,18 @@ public class ConfigMap implements ConfigStoreListener {
         *      Optional comment and blank lines to add immediately before the 
import statement.
         *      <br>If <jk>null</jk>, previous pre-lines will not be replaced.
         * @return This object.
-        * @throws UnsupportedOperationException Always - this operation is not 
implemented.
         */
        public ConfigMap setImport(String section, String importName, 
List<String> preLines) {
-               throw uoex();
+               checkSectionName(section);
+               if (! isValidConfigName(importName))
+                       throw iaex("Invalid import config name: '%s'", 
importName);
+               try (var x = lock.write()) {
+                       if (imports.stream().noneMatch(y -> 
y.getConfigName().equals(importName)))
+                               imports.add(new Import(store.getMap(importName, 
format)).register(listeners));
+               } catch (IOException e) {
+                       throw toRex(e);
+               }
+               return this;
        }
 
        /**
diff --git 
a/juneau-core/juneau-config/src/test/java/org/apache/juneau/config/Config_Test.java
 
b/juneau-core/juneau-config/src/test/java/org/apache/juneau/config/Config_Test.java
index 53488d25bb..2137f5bd09 100644
--- 
a/juneau-core/juneau-config/src/test/java/org/apache/juneau/config/Config_Test.java
+++ 
b/juneau-core/juneau-config/src/test/java/org/apache/juneau/config/Config_Test.java
@@ -1882,12 +1882,26 @@ class Config_Test extends TestBase {
        }
 
        
//====================================================================================================
-       // Config.setImport(String, String, List) - throws 
UnsupportedOperationException (not supported)
+       // Config.setImport(String, String, List) - attaches an imported config 
so its entries resolve.
        
//====================================================================================================
 
-       @Test void a53_setImport() {
-               var c = init();
-               assertThrows(UnsupportedOperationException.class, () -> 
c.setImport("", "other.cfg", null));
+       @Test void a53_setImport() throws Exception {
+               var ms = MemoryStore.create().build();
+               ms.write("A53a", "", "importedKey=1");
+               ms.write("A53b", "", "");
+               var c = Config.create("A53b").store(ms).build();
+               assertNull(c.get("importedKey").orElse(null));
+
+               c.setImport("", "A53a", null);
+               assertEquals("1", c.get("importedKey").get());
+
+               // setImport is idempotent for an already-registered import.
+               c.setImport("", "A53a", null);
+               assertEquals("1", c.get("importedKey").get());
+
+               // Read-only configs still reject the write.
+               var ro = Config.create("A53b").store(ms).readOnly().build();
+               assertThrows(UnsupportedOperationException.class, () -> 
ro.setImport("", "A53a", null));
        }
 
        
//====================================================================================================
@@ -1943,8 +1957,21 @@ class Config_Test extends TestBase {
        }
 
        @Test void a61_removeImport() throws Exception {
-               var c = init();
-               assertThrows(UnsupportedOperationException.class, () -> 
c.removeImport("", "nonexistent.cfg"));
+               var ms = MemoryStore.create().build();
+               ms.write("A61a", "", "importedKey=1");
+               ms.write("A61b", "", "<A61a>");
+               var c = Config.create("A61b").store(ms).build();
+               assertEquals("1", c.get("importedKey").get());
+
+               c.removeImport("", "A61a");
+               assertNull(c.get("importedKey").orElse(null));
+
+               // Removing a non-existent import is a no-op (does not throw).
+               c.removeImport("", "nonexistent");
+
+               // Read-only configs still reject the write.
+               var ro = Config.create("A61b").store(ms).readOnly().build();
+               assertThrows(UnsupportedOperationException.class, () -> 
ro.removeImport("", "A61a"));
        }
 
        @Test void a62_set_nullValueNonExistentKey() throws Exception {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/BeanRegistry.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/BeanRegistry.java
index 856c312352..f482dd79cc 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/BeanRegistry.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/BeanRegistry.java
@@ -110,11 +110,11 @@ public class BeanRegistry implements BeanRegistryLookup {
        /**
         * Given the specified class, return the dictionary name for it.
         *
-        * @param c The class to lookup in this registry.
+        * @param c The class to lookup in this registry. Can be <jk>null</jk>.
         * @return The dictionary name for the specified class in this 
registry, or <jk>null</jk> if not found.
         */
        public String getTypeName(ClassMeta<?> c) {
-               return isEmpty ? null : reverseMap.get(c.inner());
+               return isEmpty || c == null ? null : reverseMap.get(c.inner());
        }
 
        /**
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ClassMeta.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ClassMeta.java
index 1a40eb8fa9..a80999c6da 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ClassMeta.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ClassMeta.java
@@ -498,6 +498,27 @@ public final class ClassMeta<T> extends BeanInfo<T> {
                return beanMeta.get().beanMeta();
        }
 
+       /**
+        * Same as {@link #getBeanMeta()} but derives property names using the 
supplied {@link PropertyNamer} instead of
+        * the one configured on the owning context.
+        *
+        * <p>
+        * Unlike {@link #getBeanMeta()}, the returned {@link BeanMeta} is 
built fresh on each call and is <b>not</b>
+        * cached — the shared, {@code Class}-keyed default {@link BeanMeta} 
(which uses the context's default namer) is
+        * left untouched.  This keeps the cache safe when callers request an 
alternate property-naming strategy (e.g.
+        * {@code MarshallingSession#toBeanMap(Object, PropertyNamer)}).
+        *
+        * @param propertyNamer The property namer to use for deriving property 
names.  Must not be <jk>null</jk>.
+        * @return
+        *      A newly-built {@link BeanMeta} whose property names reflect the 
supplied namer, or <jk>null</jk> if there is
+        *      no bean meta associated with this class.
+        */
+       public BeanMeta<T> getBeanMeta(PropertyNamer propertyNamer) {
+               if (! cat.isUnknown())
+                       return null;
+               return BeanMeta.create(this, implClass.get(), 
propertyNamer).beanMeta();
+       }
+
        /**
         * Returns the bean registry for this class.
         *
@@ -1420,10 +1441,12 @@ public final class ClassMeta<T> extends BeanInfo<T> {
         * Similar to {@link #equals(Object)} except primitive and Object types 
that are similar are considered the same.
         * (e.g. <jk>boolean</jk> == <c>Boolean</c>).
         *
-        * @param cm The class meta to compare to.
+        * @param cm The class meta to compare to. Can be <jk>null</jk>.
         * @return <jk>true</jk> if the specified class-meta is equivalent to 
this one.
         */
        public boolean same(ClassMeta<?> cm) {
+               if (cm == null)
+                       return false;
                if (equals(cm))
                        return true;
                return (isPrimitive() && cat.same(cm.cat));
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingContext.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingContext.java
index dbd8fc0368..56e6a72294 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingContext.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingContext.java
@@ -3336,7 +3336,11 @@ public class MarshallingContext extends Context 
implements ConversionFinder, Bea
                 * @return This object.
                 */
                public <T,S> Builder swap(Class<T> normalClass, Class<S> 
swappedClass, ThrowingFunction<T,S> swapFunction) {
-                       return swap(normalClass, swappedClass, swapFunction, 
null);
+                       assertArgNotNull(ARG_normalClass, normalClass);
+                       assertArgNotNull(ARG_swappedClass, swappedClass);
+                       assertArgNotNull(ARG_swapFunction, swapFunction);
+                       swaps().add(0, new FunctionalSwap<>(normalClass, 
swappedClass, swapFunction, null));
+                       return this;
                }
 
                /**
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingSession.java
index b9ee19016d..5470a87384 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingSession.java
@@ -1219,16 +1219,39 @@ public class MarshallingSession extends ContextSession 
implements ConverterSessi
         *      BeanMap&lt;Person&gt; <jv>beanMap</jv> = 
MarshallingContext.<jsf>DEFAULT</jsf>.toBeanMap(<jk>new</jk> Person(), 
PropertyNamerDLC.<jsf>INSTANCE</jsf>);
         * </p>
         *
+        * <p>
+        * The supplied {@code propertyNamer} controls the names of the 
properties exposed by the returned
+        * {@link BeanMap}.  For example, passing {@link 
PropertyNamerDLC#INSTANCE} renames a <c>fooBar</c> property to
+        * <c>foo-bar</c>.
+        *
+        * <p>
+        * When {@code propertyNamer} is <jk>null</jk> or equal to this 
session's {@link #getPropertyNamer() default
+        * namer}, the shared, {@code Class}-keyed {@link BeanMeta} cache is 
used exactly as by {@link #toBeanMap(Object)}.
+        * Otherwise a fresh, non-cached {@link BeanMeta} is built with the 
supplied namer so the default cache is never
+        * polluted with alternate-namer property keys.
+        *
         * @param <T> The class of the object being wrapped.
         * @param o The object to wrap in a map interface.  Must not be null.
-        * @param propertyNamer The property namer to use.
+        * @param propertyNamer The property namer to use.  Can be 
<jk>null</jk> to use the session's default namer.
         * @return The wrapped object.
         */
-       @SuppressWarnings({
-               "java:S1172" // Parameter reserved for future property naming 
strategy support
-       })
        public final <T> BeanMap<T> toBeanMap(T o, PropertyNamer propertyNamer) 
{
-               return this.toBeanMap(o, (Class<T>)o.getClass());
+               assertArgNotNull(ARG_o, o);
+               if (o instanceof BeanMap o2)
+                       return o2;
+
+               // Preserve the cached default-namer path when no override (or 
the override matches the default) is supplied.
+               if (propertyNamer == null || 
propertyNamer.equals(getPropertyNamer()))
+                       return this.toBeanMap(o, (Class<T>)o.getClass());
+
+               var c = (Class<T>)o.getClass();
+               var cm = getClassMeta(c);
+               var m = cm.getBeanMeta(propertyNamer);
+               if (m == null)
+                       throw brex(c, "Class is not a bean.  Reason='%s'", 
cm.getNotABeanReason());
+               var bm = new BeanMap<>(o, m);
+               bm.setBeanSession(this);
+               return bm;
        }
 
        /**
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingTraverseSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingTraverseSession.java
index 4262634fcb..03a23cb5e0 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingTraverseSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/MarshallingTraverseSession.java
@@ -19,6 +19,7 @@ package org.apache.juneau.marshall;
 import static java.util.Collections.*;
 import static org.apache.juneau.commons.utils.AssertionUtils.*;
 import static org.apache.juneau.commons.utils.Shorts.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
 
 import java.util.*;
 import java.util.function.*;
@@ -306,7 +307,11 @@ public class MarshallingTraverseSession extends 
MarshallingSession {
         * @param args Optional {@link String#format(String, Object...) 
String.format}-style arguments.
         */
        protected void onError(Throwable t, String msg, Object...args) {
-               super.addWarning(msg, args);
+               // Honor the throwable per the @param contract: fold its 
localized message into the recorded warning when present.
+               if (t == null)
+                       super.addWarning(msg, args);
+               else
+                       super.addWarning("%s%n\tCaused by: %s", f(msg, args), 
localizedMessage(t));
        }
 
        /**
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/httppart/HttpPartSchema.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/httppart/HttpPartSchema.java
index 191566e7aa..d49389beb6 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/httppart/HttpPartSchema.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/httppart/HttpPartSchema.java
@@ -653,7 +653,9 @@ public class HttpPartSchema {
                 * @return This object.
                 */
                public Builder collectionFormat(HttpPartCollectionFormat value) 
{
-                       collectionFormat = value;
+                       // Guard null for parity with the String overload 
(which ine()-guards); a null must not overwrite the field.
+                       if (value != null)
+                               collectionFormat = value;
                        return this;
                }
 
@@ -1176,7 +1178,9 @@ public class HttpPartSchema {
                 * @return This object.
                 */
                public Builder format(HttpPartFormat value) {
-                       format = value;
+                       // Guard null for parity with the String overload 
(which ine()-guards); a null must not overwrite the field.
+                       if (value != null)
+                               format = value;
                        return this;
                }
 
@@ -2393,7 +2397,9 @@ public class HttpPartSchema {
                 * @return This object.
                 */
                public Builder type(HttpPartDataType value) {
-                       type = value;
+                       // Guard null for parity with the String overload 
(which ine()-guards); a null must not overwrite the field.
+                       if (value != null)
+                               type = value;
                        return this;
                }
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5/Json5List.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5/Json5List.java
index 5d2edf2a8f..1409edc98b 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5/Json5List.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5/Json5List.java
@@ -253,11 +253,8 @@ public class Json5List extends MarshalledList {
         * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
-       @SuppressWarnings({
-               "java:S1172" // Parameter reserved for future parser context 
support
-       })
        public static Json5List ofString(Reader in, Parser p) throws 
ParseException {
-               return in == null ? new Json5List() : new Json5List(in);
+               return in == null ? new Json5List() : new Json5List(in, p);
        }
 
        /**
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/prototext/PrototextTokenizer.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/prototext/PrototextTokenizer.java
index 74573636cd..539f5b6ac0 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/prototext/PrototextTokenizer.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/prototext/PrototextTokenizer.java
@@ -545,7 +545,8 @@ class PrototextTokenizer {
        }
 
        ParseException parseException(String message) {
-               return new ParseException(message + " at line " + line + ", 
column " + column);
+               // Pass the raw message as a %s arg so any literal '%' in 
echoed input can't be interpreted as a printf directive.
+               return new ParseException("%s at line %s, column %s", message, 
line, column);
        }
 
        private static boolean isLetterOrUnderscore(int c) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/toml/TomlTokenizer.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/toml/TomlTokenizer.java
index 6774be5ff4..b1dc331d20 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/toml/TomlTokenizer.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/toml/TomlTokenizer.java
@@ -149,7 +149,8 @@ class TomlTokenizer {
        }
 
        ParseException parseException(String message) {
-               return new ParseException(message + " at line " + line + ", 
column " + column);
+               // Pass the raw message as a %s arg so any literal '%' in 
echoed input can't be interpreted as a printf directive.
+               return new ParseException("%s at line %s, column %s", message, 
line, column);
        }
 
        String readBareKey() throws IOException, ParseException {
@@ -557,30 +558,30 @@ class TomlTokenizer {
                        try {
                                return 
java.time.OffsetDateTime.parse(s.replace(" ", "T"));
                        } catch (Exception e) {
-                               throw new ParseException("Invalid offset 
date-time: " + s, e);
+                               throw new ParseException(e, "Invalid offset 
date-time: %s", s);
                        }
                }
                if (s.contains("T") || s.contains(" ")) {
                        try {
                                return 
java.time.LocalDateTime.parse(s.replace(" ", "T"));
                        } catch (Exception e) {
-                               throw new ParseException("Invalid local 
date-time: " + s, e);
+                               throw new ParseException(e, "Invalid local 
date-time: %s", s);
                        }
                }
                if (s.contains("-") && s.length() == 10) {
                        try {
                                return java.time.LocalDate.parse(s);
                        } catch (Exception e) {
-                               throw new ParseException("Invalid local date: " 
+ s, e);
+                               throw new ParseException(e, "Invalid local 
date: %s", s);
                        }
                }
                if (s.contains(":")) {
                        try {
                                return java.time.LocalTime.parse(s);
                        } catch (Exception e) {
-                               throw new ParseException("Invalid local time: " 
+ s, e);
+                               throw new ParseException(e, "Invalid local 
time: %s", s);
                        }
                }
-               throw new ParseException("Invalid date-time: " + s);
+               throw new ParseException("Invalid date-time: %s", s);
        }
 }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/utils/Iso8601Utils.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/utils/Iso8601Utils.java
index 9332004999..05419af957 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/utils/Iso8601Utils.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/utils/Iso8601Utils.java
@@ -87,15 +87,17 @@ public final class Iso8601Utils {
         * {@link #formatDate}, {@link #formatCalendar}, {@link 
#formatTemporal},
         * {@link #formatDuration}, {@link #formatPeriod}.
         *
-        * @param value The value to format.  Must not be <jk>null</jk> (unlike 
the per-type helpers, this dispatcher does not null-guard).
+        * @param value The value to format.  <jk>null</jk> returns 
<jk>null</jk> (consistent with the per-type helpers).
         * @param type The class metadata for the value (used for selecting the 
appropriate formatter for Temporal types).
         * @param timeZone The session time zone (used when the value lacks 
zone info).
-        * @return The ISO 8601 string representation.
+        * @return The ISO 8601 string representation, or <jk>null</jk> if 
{@code value} is <jk>null</jk>.
         */
        @SuppressWarnings({
                "java:S1172" // type kept for API compatibility; future callers 
may use it for per-type dispatch hints
        })
        public static String format(Object value, ClassMeta<?> type, TimeZone 
timeZone) {
+               if (value == null)
+                       return null;
                if (value instanceof Duration d)
                        return formatDuration(d, 
DurationFormat.ISO_8601_WITH_DAYS);
                if (value instanceof Period p)
@@ -208,15 +210,17 @@ public final class Iso8601Utils {
        /**
         * Formats a date/time value as an ISO date (date-only, for OpenAPI 
'date' format).
         *
-        * @param value The value to format.  Must not be <jk>null</jk>.
+        * @param value The value to format.  <jk>null</jk> returns 
<jk>null</jk>.
         * @param type The class metadata.
         * @param timeZone The session time zone.
-        * @return The ISO date string.
+        * @return The ISO date string, or <jk>null</jk> if {@code value} is 
<jk>null</jk>.
         */
        @SuppressWarnings({
                "java:S1172" // type kept for API compatibility; callers pass 
ClassMeta context for potential future use
        })
        public static String formatAsDate(Object value, ClassMeta<?> type, 
TimeZone timeZone) {
+               if (value == null)
+                       return null;
                ZoneId zoneId = timeZone != null ? timeZone.toZoneId() : 
ZoneId.systemDefault();
                if (value instanceof Calendar c) {
                        ZonedDateTime zdt = (c instanceof GregorianCalendar gc) 
? gc.toZonedDateTime() : c.toInstant().atZone(c.getTimeZone().toZoneId());
@@ -232,15 +236,17 @@ public final class Iso8601Utils {
        /**
         * Formats a date/time value as an ISO date-time (for OpenAPI 
'date-time' format).
         *
-        * @param value The value to format.  Must not be <jk>null</jk>.
+        * @param value The value to format.  <jk>null</jk> returns 
<jk>null</jk>.
         * @param type The class metadata.
         * @param timeZone The session time zone.
-        * @return The ISO date-time string.
+        * @return The ISO date-time string, or <jk>null</jk> if {@code value} 
is <jk>null</jk>.
         */
        @SuppressWarnings({
                "java:S1172" // type kept for API compatibility; callers pass 
ClassMeta context for potential future use
        })
        public static String formatAsDateTime(Object value, ClassMeta<?> type, 
TimeZone timeZone) {
+               if (value == null)
+                       return null;
                ZoneId zoneId = timeZone != null ? timeZone.toZoneId() : 
ZoneId.systemDefault();
                if (value instanceof Calendar c) {
                        ZonedDateTime zdt = (c instanceof GregorianCalendar gc) 
? gc.toZonedDateTime() : c.toInstant().atZone(c.getTimeZone().toZoneId());
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/BeanRegistry_Test.java
similarity index 55%
copy from 
juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
copy to 
juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/BeanRegistry_Test.java
index 854e75ea81..9a11a01c44 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/BeanRegistry_Test.java
@@ -14,19 +14,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.juneau.rest.server.beans;
+package org.apache.juneau.marshall;
 
 import static org.junit.jupiter.api.Assertions.*;
 
 import org.apache.juneau.*;
-import org.apache.juneau.marshall.marshaller.*;
 import org.junit.jupiter.api.*;
 
-class ResourceDescription_Test extends TestBase {
+/**
+ * Validates {@link BeanRegistry}.
+ */
+class BeanRegistry_Test extends TestBase {
+
+       @Marshalled(typeName="A")
+       public static class A {
+               public String f;
+       }
+
+       // Regression: getTypeName(ClassMeta) NPE'd on a null argument for a 
non-empty registry (it dereferenced
+       // c.inner()), even though its sibling getTypeName(Class) guarded null 
and both share the "or null" @return.
+       @Test void getTypeName_nullClassMeta() {
+               var bc = 
MarshallingContext.create().beanDictionary(A.class).build();
+               var reg = bc.getBeanRegistry();
 
-       @Test void a01_basic() throws Exception {
-               var rd = new ResourceDescription("a","b?c=d&e=f","g");
-               assertEquals("<table><tr><td>name</td><td><a 
href=\"/b?c=d&amp;e=f\">a</a></td></tr><tr><td>description</td><td>g</td></tr></table>",
 Html.of(rd));
-               assertEquals("{name:'a',description:'g'}", Json5.of(rd));
+               assertEquals("A", reg.getTypeName(bc.getClassMeta(A.class)));
+               assertNull(reg.getTypeName((ClassMeta<?>)null));
+               assertNull(reg.getTypeName((Class<?>)null));
        }
-}
\ No newline at end of file
+}
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/ClassMeta_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/ClassMeta_Test.java
index 7edbb05a1a..e8482bd813 100755
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/ClassMeta_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/ClassMeta_Test.java
@@ -320,4 +320,15 @@ class ClassMeta_Test extends TestBase {
                c3.forEachAnnotation(A.class, x -> x.value() == 5, x -> 
l4.add(x.value()));
                assertList(l4, "5");
        }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // same(null)
+       // Regression: same(null) returned false for non-primitive types but 
NPE'd for primitive types
+       // (the primitive branch dereferenced the null arg via 
cat.same(cm.cat)).  Both must return false now.
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void same_nullArg() {
+               assertFalse(bc.getClassMeta(String.class).same(null));
+               assertFalse(bc.getClassMeta(int.class).same(null));
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/MarshallingContext_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/MarshallingContext_Test.java
index 00f6c22410..b9783ac690 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/MarshallingContext_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/MarshallingContext_Test.java
@@ -713,4 +713,26 @@ class BeanContext_Test extends TestBase {
                var cm = bc.getClassMeta(List.class, String.class);
                assertNotNull(cm);
        }
+
+       
//====================================================================================================
+       // Functional swap(...) builder overloads.
+       // Regression: the 3-arg serialize-only swap(nc,sc,swapFunction) 
overload used to delegate through
+       // the 4-arg overload's assertArgNotNull(unswapFunction) and therefore 
threw on EVERY call, so a
+       // serialize-only swap could never be registered.
+       
//====================================================================================================
+
+       public static class PSwapBean {
+               public String value;
+               public PSwapBean(String value) { this.value = value; }
+       }
+
+       @Test void p01_functionalSwap_serializeOnly() throws Exception {
+               var s = Json5Serializer.create().swap(PSwapBean.class, 
String.class, x -> x.value).build();
+               assertEquals("'foo'", s.write(new PSwapBean("foo")));
+       }
+
+       @Test void p02_functionalSwap_twoWay() throws Exception {
+               var s = Json5Serializer.create().swap(PSwapBean.class, 
String.class, x -> x.value, PSwapBean::new).build();
+               assertEquals("'foo'", s.write(new PSwapBean("foo")));
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/MarshallingSession_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/MarshallingSession_Test.java
new file mode 100644
index 0000000000..5033cb1f81
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/MarshallingSession_Test.java
@@ -0,0 +1,97 @@
+/*
+ * 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.marshall;
+
+import static org.apache.juneau.test.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.bean.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link MarshallingSession#toBeanMap(Object, PropertyNamer)}.
+ */
+class MarshallingSession_Test extends TestBase {
+
+       MarshallingContext bc = MarshallingContext.DEFAULT;
+       MarshallingSession bs = MarshallingContext.DEFAULT_SESSION;
+
+       public static class A {
+               public String fooBar = "x";
+               public String qux = "y";
+       }
+
+       
//====================================================================================================
+       // toBeanMap(Object, PropertyNamer) honors the supplied namer for 
property names.
+       
//====================================================================================================
+
+       @Test void a01_dashedLowerCaseNamer() {
+               var m = bs.toBeanMap(new A(), PropertyNamerDLC.INSTANCE);
+               assertList(m.keySet(), "foo-bar", "qux");
+               assertEquals("x", m.get("foo-bar"));
+               assertEquals("y", m.get("qux"));
+       }
+
+       @Test void a02_dashedUpperCaseStartNamer() {
+               var m = bs.toBeanMap(new A(), PropertyNamerDUCS.INSTANCE);
+               assertList(m.keySet(), "Foo-Bar", "Qux");
+               assertEquals("x", m.get("Foo-Bar"));
+               assertEquals("y", m.get("Qux"));
+       }
+
+       public static class A03_UpperNamer implements PropertyNamer {
+               @Override /* Overridden from PropertyNamer */
+               public String getPropertyName(String name) {
+                       return name == null ? null : name.toUpperCase();
+               }
+       }
+
+       @Test void a03_customAllCapsNamer() {
+               var m = bs.toBeanMap(new A(), new A03_UpperNamer());
+               assertList(m.keySet(), "FOOBAR", "QUX");
+               assertEquals("x", m.get("FOOBAR"));
+       }
+
+       
//====================================================================================================
+       // The null / default-namer path is unchanged (uses the shared, 
Class-keyed BeanMeta cache).
+       
//====================================================================================================
+
+       @Test void b01_nullNamerMatchesDefault() {
+               var m = bs.toBeanMap(new A(), (PropertyNamer)null);
+               var expected = bs.toBeanMap(new A());
+               assertList(m.keySet(), "fooBar", "qux");
+               assertEquals(expected.keySet(), m.keySet());
+       }
+
+       @Test void b02_defaultNamerUsesCachedBeanMeta() {
+               // Passing the session's own default namer must reuse the 
cached BeanMeta (same instance as no-namer path).
+               var m = bs.toBeanMap(new A(), bs.getPropertyNamer());
+               var cached = bc.getClassMeta(A.class).getBeanMeta();
+               assertSame(cached, m.getMeta());
+               assertList(m.keySet(), "fooBar", "qux");
+       }
+
+       @Test void b03_overrideNamerDoesNotPolluteCache() {
+               // Building an alternate-namer BeanMap must not change the 
shared cached BeanMeta's property names.
+               var before = bc.getClassMeta(A.class).getBeanMeta();
+               bs.toBeanMap(new A(), PropertyNamerDLC.INSTANCE);
+               var after = bc.getClassMeta(A.class).getBeanMeta();
+               assertSame(before, after);
+               assertList(after.getProperties().keySet(), "fooBar", "qux");
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/httppart/HttpPartSchema_Validation_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/httppart/HttpPartSchema_Validation_Test.java
index d035174179..79dd61ddde 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/httppart/HttpPartSchema_Validation_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/httppart/HttpPartSchema_Validation_Test.java
@@ -1422,4 +1422,22 @@ class HttpPartSchema_Validation_Test extends TestBase {
                }
                assertTrue(levels >= depth, "Expected the nested schema to be 
reachable to full depth");
        }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Enum-setter null parity
+       // Regression: 
type(HttpPartDataType)/format(HttpPartFormat)/collectionFormat(HttpPartCollectionFormat)
 had no
+       // null guard (unlike their ine()-guarded String overloads), so with 
noValidate() an explicit null overwrote the
+       // non-null default and the getter returned null.  A null enum must now 
be ignored (default preserved).
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void u01_enumSetters_nullIgnored() {
+               var s = HttpPartSchema.create().noValidate()
+                       .type((HttpPartDataType)null)
+                       .format((HttpPartFormat)null)
+                       .collectionFormat((HttpPartCollectionFormat)null)
+                       .build();
+               assertEquals(HttpPartDataType.NO_TYPE, s.getType());
+               assertEquals(HttpPartFormat.NO_FORMAT, s.getFormat());
+               assertEquals(HttpPartCollectionFormat.NO_COLLECTION_FORMAT, 
s.getCollectionFormat());
+       }
 }
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5/Json5List_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5/Json5List_Test.java
index 630fc20c8d..9f2cfa6e14 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5/Json5List_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5/Json5List_Test.java
@@ -23,6 +23,8 @@ import java.io.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.parser.*;
 import org.junit.jupiter.api.*;
 
 /**
@@ -124,6 +126,17 @@ class Json5List_Test extends TestBase {
                assertEquals(3, l.size());
        }
 
+       @Test void a07b_ofStringReaderHonorsParser() throws Exception {
+               // Regression: ofString(Reader, Parser) ignored its Parser p 
and always used Json5Parser.DEFAULT.
+               // Json5Parser allows JS comments; the strict JsonParser 
rejects them ("Javascript comment detected"),
+               // so honoring p makes the strict parser throw where the 
ignored-parser path silently succeeded.
+               var json5WithComment = "[1,/*c*/2,3]";
+               assertEquals(3, Json5List.ofString(new 
StringReader(json5WithComment), Json5Parser.DEFAULT).size());
+               assertThrows(ParseException.class, () -> Json5List.ofString(new 
StringReader(json5WithComment), JsonParser.DEFAULT));
+               // The CharSequence overload already honored p — the Reader 
overload now matches it.
+               assertThrows(ParseException.class, () -> 
Json5List.ofString(json5WithComment, JsonParser.DEFAULT));
+       }
+
        @Test void a07_ctorReaderUsesJson5Parser() throws Exception {
                var l = new Json5List(new StringReader("[1,2,3]"));
                assertEquals(3, l.size());
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/prototext/PrototextTokenizer_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/prototext/PrototextTokenizer_Test.java
index 95744c9327..e43f17f67b 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/prototext/PrototextTokenizer_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/prototext/PrototextTokenizer_Test.java
@@ -613,4 +613,22 @@ class PrototextTokenizer_Test {
                assertThrows(ParseException.class, () ->
                        PrototextParser.DEFAULT.read("n: 
0777777777777777777777777", JsonMap.class));
        }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // parseException printf-injection safety.
+       // A literal '%' echoed from an offending token must survive verbatim 
and must not be interpreted as a
+       // printf directive (which would previously mangle the message or 
throw).
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void c01_parseExceptionLiteralPercentNotFormatted() {
+               var t = tok("");
+               var ex = t.parseException("Unexpected character: %");
+               assertTrue(ex.getMessage().startsWith("Unexpected character: % 
at line "));
+       }
+
+       @Test void c02_parseExceptionUnexpectedCharPercent() {
+               // Drives an actual parse error whose echoed character is a 
literal '%'.
+               var ex = assertThrows(ParseException.class, () -> 
tok("%").peek());
+               assertTrue(ex.getMessage().contains("Unexpected character: %"));
+       }
 }
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/toml/TomlTokenizer_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/toml/TomlTokenizer_Test.java
index 43a9f821a9..8593e450d7 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/toml/TomlTokenizer_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/toml/TomlTokenizer_Test.java
@@ -873,4 +873,22 @@ class TomlTokenizer_Test extends TestBase {
                assertTrue(ex.getMessage().contains("line"));
                assertTrue(ex.getMessage().contains("column"));
        }
+
+       @Test
+       void p02_parseExceptionLiteralPercentNotFormatted() {
+               // A literal '%' in the message must survive verbatim (no 
printf re-interpretation).
+               var tok = t("");
+               var ex = tok.parseException("bad token: 100%");
+               assertTrue(ex.getMessage().startsWith("bad token: 100% at line 
"));
+       }
+
+       @Test
+       void p03_parseDateTimeStringPercentPreservesMessageAndCause() {
+               // Previously new ParseException("Invalid offset date-time: " + 
s, e) bound to (String, Object...):
+               // the cause 'e' was dropped and a stray '%' in the scanned 
value was re-interpreted as a printf
+               // directive (throwing a formatting exception).  The fix passes 
'e' as the cause and 's' as a %s arg.
+               var ex = assertThrows(ParseException.class, () -> 
TomlTokenizer.parseDateTimeString("1979-05-27T07:32:00%Z"));
+               assertTrue(ex.getMessage().contains("1979-05-27T07:32:00%Z"));
+               assertNotNull(ex.getCause());
+       }
 }
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/utils/Iso8601Utils_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/utils/Iso8601Utils_Test.java
index 033a19d081..2f81f44580 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/utils/Iso8601Utils_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/utils/Iso8601Utils_Test.java
@@ -83,6 +83,14 @@ class Iso8601Utils_Test extends TestBase {
                assertEquals("some-string", r);
        }
 
+       @Test void a07_format_nullValue() {
+               // Regression: the three dispatchers deref'd value unguarded 
(NPE on null), unlike the per-type helpers
+               // that all return null.  Aligned per Q4 — null value now 
returns null.
+               assertNull(Iso8601Utils.format(null, 
BC.getClassMeta(String.class), UTC));
+               assertNull(Iso8601Utils.formatAsDate(null, 
BC.getClassMeta(Date.class), UTC));
+               assertNull(Iso8601Utils.formatAsDateTime(null, 
BC.getClassMeta(Date.class), UTC));
+       }
+
        
//------------------------------------------------------------------------------------------------------------------
        // formatDuration — null format defaults to ISO_8601_WITH_DAYS (line 
122), null value (line 120)
        
//------------------------------------------------------------------------------------------------------------------
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
index 854e75ea81..18f1f9eab3 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
@@ -29,4 +29,23 @@ class ResourceDescription_Test extends TestBase {
                assertEquals("<table><tr><td>name</td><td><a 
href=\"/b?c=d&amp;e=f\">a</a></td></tr><tr><td>description</td><td>g</td></tr></table>",
 Html.of(rd));
                assertEquals("{name:'a',description:'g'}", Json5.of(rd));
        }
+
+       // Regression: hashCode()/compareTo() dereferenced getName() unguarded, 
so an unpopulated ResourceDescription
+       // (public no-arg ctor leaves 'name' null) NPE'd when hashed, sorted, 
or added to a TreeSet — inconsistent with
+       // the already-null-safe equals().
+       @Test void a02_nullNameIsHashableAndComparable() {
+               var empty = new ResourceDescription();
+               assertEquals(0, empty.hashCode());
+               assertEquals(0, empty.compareTo(new ResourceDescription()));
+
+               var named = new ResourceDescription().name("a");
+               assertTrue(empty.compareTo(named) < 0);
+               assertTrue(named.compareTo(empty) > 0);
+
+               // Can be used as a TreeSet element without NPE.
+               var set = new java.util.TreeSet<ResourceDescription>();
+               set.add(empty);
+               set.add(named);
+               assertEquals(2, set.size());
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/debug/DebugConfig_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/debug/DebugConfig_Test.java
index 9385dad092..d93ec2cf7d 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/debug/DebugConfig_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/debug/DebugConfig_Test.java
@@ -190,6 +190,8 @@ class DebugConfig_Test extends TestBase {
                assertNotNull(rule, "rule must be retrievable via getRuleFor");
                assertEquals(Level.FINE, rule.getLevel());
                assertNull(cfg.getRuleFor("com.example.Bar"), "no rule should 
be returned for unconfigured target");
+               // Regression: getRuleFor(null) NPE'd on the null-hostile 
Map.copyOf(...) map despite the "or null" @return.
+               assertNull(cfg.getRuleFor(null), "null target must return null, 
not NPE");
        }
 
        @Test void b09_chainedBuilder_allFluent() {
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestAttributes_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestAttributes_Test.java
index cb3fe54ae5..f657e67959 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestAttributes_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestAttributes_Test.java
@@ -158,6 +158,15 @@ class RequestAttributes_Test extends TestBase {
                        var s = attrs.toString();
                        return "len=" + (!s.isEmpty()) + ",hasTs=" + 
s.contains("ts");
                }
+
+               // Regression: addDefault(null) NPE'd for all three "Can be 
null" overloads.
+               @RestGet(path="/addDefaultNullIsNoOp")
+               public String addDefaultNullIsNoOp(RequestAttributes attrs) {
+                       attrs.addDefault((NamedAttribute[])null);
+                       attrs.addDefault((java.util.List<NamedAttribute>)null);
+                       attrs.addDefault((NamedAttributeMap)null);
+                       return "ok";
+               }
        }
 
        
//------------------------------------------------------------------------------------------------------------------
@@ -213,4 +222,8 @@ class RequestAttributes_Test extends TestBase {
        @Test void a11_toString_isInformative() throws Exception {
                
client().get("/toString").run().assertContent("len=true,hasTs=true");
        }
+
+       @Test void a12_addDefault_nullIsNoOp() throws Exception {
+               client().get("/addDefaultNullIsNoOp").run().assertContent("ok");
+       }
 }
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestQueryParamList_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestQueryParamList_Test.java
index fc7ee6096f..bfe4365888 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestQueryParamList_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestQueryParamList_Test.java
@@ -451,6 +451,14 @@ class RequestQueryParamList_Test extends TestBase {
                                return "iae:" + e.getMessage().contains("name");
                        }
                }
+               // Regression: addDefault(null) NPE'd — "Can be null" pairs 
must be tolerated as an empty default set.
+               @RestGet(path="/addDefaultNullIsNoOp")
+               public String addDefaultNullIsNoOp(RequestQueryParamList q) {
+                       var before = q.size();
+                       q.addDefault((HttpPart[])null);
+                       q.addDefault((java.util.List<? extends HttpPart>)null);
+                       return "size=" + before + "->" + q.size();
+               }
        }
 
        @Test
@@ -465,4 +473,10 @@ class RequestQueryParamList_Test extends TestBase {
                c.get("/getFirstNullThrows").run().assertContent("iae:true");
                c.get("/getLastNullThrows").run().assertContent("iae:true");
        }
+
+       @Test
+       void b02_addDefaultNullIsNoOp() throws Exception {
+               var c = MockRestClient.build(B.class);
+               
c.get("/addDefaultNullIsNoOp?x=1").run().assertContent("size=1->1");
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/remote/RemoteOperationArg.java
 
b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/remote/RemoteOperationArg.java
index 080fa49c2a..55be960a11 100644
--- 
a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/remote/RemoteOperationArg.java
+++ 
b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/remote/RemoteOperationArg.java
@@ -117,7 +117,7 @@ public class RemoteOperationArg {
        /**
         * Returns the HTTP part serializer to use for serializing this part.
         *
-        * @return The HTTP part serializer, or the default if not specified.
+        * @return The HTTP part serializer, or an empty {@link Optional} if 
not specified.
         */
        public Optional<HttpPartSerializer> getSerializer() { return 
serializer; }
 
diff --git 
a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/ContentSecurityPolicy.java
 
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/ContentSecurityPolicy.java
index 0054df2a3e..494c45f86c 100644
--- 
a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/ContentSecurityPolicy.java
+++ 
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/ContentSecurityPolicy.java
@@ -491,7 +491,7 @@ public class ContentSecurityPolicy extends HttpStringHeader 
{
                 * Replaces any previously-registered source list for the same 
directive while preserving its original position.
                 *
                 * @param name The directive name (e.g. <js>"worker-src"</js>). 
Must not be <jk>null</jk> or blank.
-                * @param sources The source expressions. May be empty (renders 
the bare directive name).
+                * @param sources The source expressions. May be empty or 
<jk>null</jk> (renders the bare directive name).
                 * @return This object.
                 * @throws IllegalArgumentException If {@code name} is 
<jk>null</jk> or blank.
                 */
@@ -500,7 +500,7 @@ public class ContentSecurityPolicy extends HttpStringHeader 
{
                        var n = name.trim();
                        if (n.isEmpty())
                                throw iaex("directive name must not be blank");
-                       directives.put(n, new 
ArrayList<>(Arrays.asList(sources)));
+                       directives.put(n, sources == null ? new ArrayList<>() : 
new ArrayList<>(Arrays.asList(sources)));
                        return this;
                }
 
diff --git 
a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/remote/RrpcInterfaceMethodMeta.java
 
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/remote/RrpcInterfaceMethodMeta.java
index d9e1e23cbd..4306a098ce 100644
--- 
a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/remote/RrpcInterfaceMethodMeta.java
+++ 
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/remote/RrpcInterfaceMethodMeta.java
@@ -347,7 +347,7 @@ public final class RrpcInterfaceMethodMeta {
         * @return The default value, or <jk>null</jk>.
         */
        public String getHeaderDefault(String name) {
-               return headerDefaults.get(name);
+               return name == null ? null : headerDefaults.get(name);
        }
 
        /**
@@ -357,7 +357,7 @@ public final class RrpcInterfaceMethodMeta {
         * @return The default value, or <jk>null</jk>.
         */
        public String getQueryDefault(String name) {
-               return queryDefaults.get(name);
+               return name == null ? null : queryDefaults.get(name);
        }
 
        /**
@@ -367,7 +367,7 @@ public final class RrpcInterfaceMethodMeta {
         * @return The default value, or <jk>null</jk>.
         */
        public String getFormDataDefault(String name) {
-               return formDataDefaults.get(name);
+               return name == null ? null : formDataDefaults.get(name);
        }
 
        /**
@@ -377,7 +377,7 @@ public final class RrpcInterfaceMethodMeta {
         * @return The default value, or <jk>null</jk>.
         */
        public String getPathDefault(String name) {
-               return pathDefaults.get(name);
+               return name == null ? null : pathDefaults.get(name);
        }
 
        /**
diff --git 
a/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/header/ContentSecurityPolicy_Test.java
 
b/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/header/ContentSecurityPolicy_Test.java
index 4a82a5e576..e6835e3515 100644
--- 
a/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/header/ContentSecurityPolicy_Test.java
+++ 
b/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/header/ContentSecurityPolicy_Test.java
@@ -146,6 +146,13 @@ class ContentSecurityPolicy_Test extends TestBase {
                @Test void b10_blankDirectiveNameThrows() {
                        assertThrowsWithMessage(IllegalArgumentException.class, 
"directive name must not be blank", () -> create().directive("  ", SELF));
                }
+
+               @Test void b11_nullSourcesRendersBareDirective() {
+                       // Regression: directive(name, (String[])null) NPE'd on 
Arrays.asList((String[])null); a null sources
+                       // array must be treated the same as empty (renders the 
bare directive name).
+                       var x = create().directive("upgrade-insecure-requests", 
(String[])null).build();
+                       assertEquals("upgrade-insecure-requests", x);
+               }
        }
 
        
//------------------------------------------------------------------------------------------------------------------
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
 
b/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/remote/RrpcInterfaceMethodMeta_Test.java
similarity index 56%
copy from 
juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
copy to 
juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/remote/RrpcInterfaceMethodMeta_Test.java
index 854e75ea81..ca25bf00cb 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/beans/ResourceDescription_Test.java
+++ 
b/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/remote/RrpcInterfaceMethodMeta_Test.java
@@ -14,19 +14,27 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.juneau.rest.server.beans;
+package org.apache.juneau.http.remote;
 
 import static org.junit.jupiter.api.Assertions.*;
 
 import org.apache.juneau.*;
-import org.apache.juneau.marshall.marshaller.*;
 import org.junit.jupiter.api.*;
 
-class ResourceDescription_Test extends TestBase {
+/**
+ * Validates {@link RrpcInterfaceMethodMeta}.
+ */
+class RrpcInterfaceMethodMeta_Test extends TestBase {
+
+       // Regression: 
getHeaderDefault/getQueryDefault/getFormDataDefault/getPathDefault(null) NPE'd 
because they
+       // delegated straight into a null-hostile Map.of()-backed map, despite 
each documenting an "or null" @return.
+       @Test void getXxxDefault_nullName() throws Exception {
+               var m = Object.class.getMethod("toString");
+               var meta = new RrpcInterfaceMethodMeta(m, "POST", "/x", 
RemoteReturn.BODY);
 
-       @Test void a01_basic() throws Exception {
-               var rd = new ResourceDescription("a","b?c=d&e=f","g");
-               assertEquals("<table><tr><td>name</td><td><a 
href=\"/b?c=d&amp;e=f\">a</a></td></tr><tr><td>description</td><td>g</td></tr></table>",
 Html.of(rd));
-               assertEquals("{name:'a',description:'g'}", Json5.of(rd));
+               assertNull(meta.getHeaderDefault(null));
+               assertNull(meta.getQueryDefault(null));
+               assertNull(meta.getFormDataDefault(null));
+               assertNull(meta.getPathDefault(null));
        }
-}
\ No newline at end of file
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpServerConfig.java
 
b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpServerConfig.java
index 519f66b43a..64bdfeb4e0 100644
--- 
a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpServerConfig.java
+++ 
b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpServerConfig.java
@@ -65,7 +65,7 @@ public class McpServerConfig {
        /**
         * MCP protocol revision returned by {@code initialize}.
         *
-        * @return The protocol version.
+        * @return The protocol version, or <jk>null</jk> if not set.
         */
        public String getProtocolVersion() {
                return protocolVersion;
@@ -74,7 +74,7 @@ public class McpServerConfig {
        /**
         * Sets the protocol revision.
         *
-        * @param protocolVersion The new value.
+        * @param protocolVersion The new value. Can be <jk>null</jk>.
         * @return This object (for method chaining).
         */
        public McpServerConfig setProtocolVersion(String protocolVersion) {
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/arg/CookieList.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/arg/CookieList.java
index 1e1c9e91a4..57b2c95438 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/arg/CookieList.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/arg/CookieList.java
@@ -38,7 +38,9 @@ public class CookieList extends ArrayList<Cookie> {
        /**
         * Static creator.
         *
-        * @param values The values to set in the cookie list. Must not be 
<jk>null</jk>.
+        * @param values
+        *      The values to set in the cookie list.
+        *      <br>Can be <jk>null</jk> (e.g. from {@link 
jakarta.servlet.http.HttpServletRequest#getCookies()} on a cookie-less 
request), treated as an empty list.
         * @return A new cookie list.
         */
        public static CookieList of(Cookie[] values) {
@@ -48,9 +50,11 @@ public class CookieList extends ArrayList<Cookie> {
        /**
         * Constructor.
         *
-        * @param values The values to set in the cookie list. Must not be 
<jk>null</jk>.
+        * @param values
+        *      The values to set in the cookie list.
+        *      <br>Can be <jk>null</jk> (e.g. from {@link 
jakarta.servlet.http.HttpServletRequest#getCookies()} on a cookie-less 
request), treated as an empty list.
         */
        public CookieList(Cookie[] values) {
-               super(l(values));
+               super(values == null ? l() : l(values));
        }
 }
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/assertions/FluentRequestHeaderAssertion.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/assertions/FluentRequestHeaderAssertion.java
index 4d5322a1d2..8140424327 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/assertions/FluentRequestHeaderAssertion.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/assertions/FluentRequestHeaderAssertion.java
@@ -143,7 +143,7 @@ public class FluentRequestHeaderAssertion<R> extends 
FluentStringAssertion<R> {
         * used on the same assertion.
         */
        public FluentRequestHeaderAssertion(Assertion creator, RequestHeader 
value, R returns) {
-               super(null, value.asString().orElse(null), returns);
+               super(creator, value.asString().orElse(null), returns);
                this.value = value;
                setThrowable(BadRequest.class);
        }
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/beans/ResourceDescription.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/beans/ResourceDescription.java
index a8573c9367..7ddf128293 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/beans/ResourceDescription.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/beans/ResourceDescription.java
@@ -77,7 +77,8 @@ public class ResourceDescription implements 
Comparable<ResourceDescription> {
 
        @Override /* Overridden from Comparable */
        public int compareTo(ResourceDescription o) {
-               return getName().compareTo(o.getName());
+               // Null-safe: the no-arg ctor and null-accepting ctors leave 
'name' null (consistent with the null-safe equals()).
+               return cmp(getName(), o.getName());
        }
 
        /**
@@ -120,7 +121,8 @@ public class ResourceDescription implements 
Comparable<ResourceDescription> {
 
        @Override /* Overridden from Object */
        public int hashCode() {
-               return getName().hashCode();
+               // Null-safe: the no-arg ctor and null-accepting ctors leave 
'name' null (consistent with the null-safe equals()).
+               return getName() == null ? 0 : getName().hashCode();
        }
 
        /**
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/debug/BasicDebugEnablement.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/debug/BasicDebugEnablement.java
index a2ef2e5f5a..f66d7c1fa2 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/debug/BasicDebugEnablement.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/debug/BasicDebugEnablement.java
@@ -38,7 +38,7 @@ import org.apache.juneau.rest.server.*;
  * <ul>
  *     <li class='ja'>{@link Rest#debug()}
  *     <li class='ja'>{@link RestOp#debug()}
- *     <li class='ja'>{@link Rest#debug()}
+ *     <li class='jm'>{@link Debug#on()}
  * </ul>
  *
  * <h5 class='section'>See Also:</h5><ul>
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/debug/DebugConfig.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/debug/DebugConfig.java
index f55b2cadc6..de66fd744a 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/debug/DebugConfig.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/debug/DebugConfig.java
@@ -219,6 +219,6 @@ public class DebugConfig {
         * @return The configured rule, or <jk>null</jk>.
         */
        public DebugRule getRuleFor(String target) {
-               return rules.get(target);
+               return target == null ? null : rules.get(target);
        }
 }
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestAttributes.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestAttributes.java
index 6815bfcb7b..db58c6b7ff 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestAttributes.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestAttributes.java
@@ -129,6 +129,8 @@ public class RequestAttributes {
         * @return This object.
         */
        public RequestAttributes addDefault(List<NamedAttribute> pairs) {
+               if (pairs == null)
+                       return this;
                for (var p : pairs)
                        if (sreq.getAttribute(p.getName()) == null) {
                                Object o = p.getValue();
@@ -158,6 +160,8 @@ public class RequestAttributes {
         * @return This object.
         */
        public RequestAttributes addDefault(NamedAttributeMap pairs) {
+               if (pairs == null)
+                       return this;
                for (var p : pairs.values())
                        if (sreq.getAttribute(p.getName()) == null) {
                                Object o = p.getValue();
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestFormParamList.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestFormParamList.java
index bf521d7fc3..c64c74fb9e 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestFormParamList.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestFormParamList.java
@@ -279,6 +279,8 @@ public class RequestFormParamList extends 
ArrayList<RequestFormParam> {
         * @return This object.
         */
        public RequestFormParamList addDefault(List<? extends HttpPart> pairs) {
+               if (pairs == null)
+                       return this;
                for (var p : pairs) {
                        var name = p.getName();
                        var l = stream(name);
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestPathParamList.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestPathParamList.java
index d0abf95e19..2c1d149f92 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestPathParamList.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestPathParamList.java
@@ -224,6 +224,8 @@ public class RequestPathParamList extends 
ArrayList<RequestPathParam> {
         * @return This object.
         */
        public RequestPathParamList addDefault(List<HttpPart> pairs) {
+               if (pairs == null)
+                       return this;
                for (var p : pairs) {
                        var name = p.getName();
                        var l = stream(name);
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestQueryParamList.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestQueryParamList.java
index f227078012..e8d3051ef8 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestQueryParamList.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestQueryParamList.java
@@ -239,6 +239,8 @@ public class RequestQueryParamList extends 
ArrayList<RequestQueryParam> {
         * @return This object.
         */
        public RequestQueryParamList addDefault(List<? extends HttpPart> pairs) 
{
+               if (pairs == null)
+                       return this;
                for (var p : pairs) {
                        var name = p.getName();
                        var l = stream(name);
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/swagger/SwaggerException.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/swagger/SwaggerException.java
index 41ad6cae6f..7dcb95d2f3 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/swagger/SwaggerException.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/swagger/SwaggerException.java
@@ -16,8 +16,6 @@
  */
 package org.apache.juneau.rest.server.swagger;
 
-import static org.apache.juneau.commons.utils.Shorts.*;
-
 import org.apache.juneau.marshall.parser.*;
 
 @SuppressWarnings({
@@ -27,6 +25,8 @@ class SwaggerException extends ParseException {
        private static final long serialVersionUID = 1L;
 
        SwaggerException(Exception e, String location, Object...locationArgs) {
-               super(e, "Swagger exception:  at " + f(location, locationArgs));
+               // Format exactly once (via the parent ctor) so a literal '%' 
surviving from location/locationArgs
+               // can't be re-interpreted as a printf directive on a second 
pass.
+               super(e, "Swagger exception:  at " + location, locationArgs);
        }
 }
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/widget/MenuItemWidget.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/widget/MenuItemWidget.java
index 2a98d754c4..e0a1a2f865 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/widget/MenuItemWidget.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/widget/MenuItemWidget.java
@@ -60,7 +60,7 @@ public abstract class MenuItemWidget extends Widget {
         *
         * <p class='bjava'>
         *      <ja>@Override</ja>
-        *      <jk>public</jk> String getBeforeShowScript(RestRequest 
<jv>req</jv>) {
+        *      <jk>public</jk> String getBeforeShowScript(RestRequest 
<jv>req</jv>, RestResponse <jv>res</jv>) {
         *              <jk>return</jk> <js>""</js>
         *                      + <js>"\n       var xhr = new 
XMLHttpRequest();"</js>
         *                      + <js>"\n       xhr.open('GET', 
'/petstore/pet?s=status=AVAILABLE&amp;v=id,name', true);"</js>
@@ -88,8 +88,8 @@ public abstract class MenuItemWidget extends Widget {
         *
         * <p class='bjava'>
         *      <ja>@Override</ja>
-        *      <jk>public</jk> String getBeforeShowScript(RestRequest 
<jv>req</jv>) <jk>throws</jk> Exception {
-        *              <jk>return</jk> 
loadScript(<js>"AddOrderMenuItem_beforeShow.js"</js>);
+        *      <jk>public</jk> String getBeforeShowScript(RestRequest 
<jv>req</jv>, RestResponse <jv>res</jv>) <jk>throws</jk> Exception {
+        *              <jk>return</jk> loadScript(<jv>req</jv>, 
<js>"AddOrderMenuItem_beforeShow.js"</js>);
         *      }
         * </p>
         *
diff --git 
a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/arg/CookieList_Test.java
 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/arg/CookieList_Test.java
new file mode 100644
index 0000000000..90cf363065
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/arg/CookieList_Test.java
@@ -0,0 +1,48 @@
+/*
+ * 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.server.arg;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+import jakarta.servlet.http.*;
+
+/**
+ * Validates {@link CookieList}.
+ *
+ * <p>
+ * Regression: the constructor did {@code super(l(values))}, and {@code 
l(null)} returns {@code null}, so
+ * {@code new CookieList(null)} threw a {@link NullPointerException}.  {@code 
HttpServletRequest.getCookies()}
+ * returns {@code null} for a cookie-less request (servlet spec), so any 
{@code @RestOp} method declaring a
+ * {@code CookieList} parameter NPE'd on a request with no cookies.
+ */
+class CookieList_Test extends TestBase {
+
+       @Test void a01_nullCookiesYieldsEmptyList() {
+               assertTrue(CookieList.of(null).isEmpty());
+               assertTrue(new CookieList(null).isEmpty());
+       }
+
+       @Test void a02_populatedCookiesPreserved() {
+               var l = CookieList.of(new Cookie[]{new Cookie("a", "1"), new 
Cookie("b", "2")});
+               assertEquals(2, l.size());
+               assertEquals("a", l.get(0).getName());
+               assertEquals("b", l.get(1).getName());
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/assertions/FluentRequestHeaderAssertion_Test.java
 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/assertions/FluentRequestHeaderAssertion_Test.java
new file mode 100644
index 0000000000..0061b9395e
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/assertions/FluentRequestHeaderAssertion_Test.java
@@ -0,0 +1,62 @@
+/*
+ * 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.server.assertions;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.server.httppart.*;
+import org.apache.juneau.test.assertions.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link FluentRequestHeaderAssertion}.
+ *
+ * <p>
+ * Regression: the chained constructor used to hard-code {@code super(null, 
...)}, discarding its {@code creator}
+ * argument, so config (custom failure message, output stream, etc.) set on 
the creating assertion was lost when a
+ * header assertion was built via the chained constructor.  The sibling 
query/form-param assertions correctly pass
+ * {@code super(creator, ...)}.
+ */
+class FluentRequestHeaderAssertion_Test extends TestBase {
+
+       @Test void a01_creatorMsgPropagates() {
+               var header = mock(RequestHeader.class);
+               when(header.asString()).thenReturn(Optional.of("actual"));
+
+               // The custom failure message configured on the creating 
assertion must propagate to the header assertion
+               // (it did not before the fix, because the chained ctor 
hard-coded super(null, ...)).
+               var creator = new FluentStringAssertion<>("ignored", 
null).setMsg("CUSTOM {msg}");
+               var h = new FluentRequestHeaderAssertion<>(creator, header, 
null);
+
+               var e = assertThrows(RuntimeException.class, () -> 
h.is("expected"));
+               assertTrue(e.getMessage().startsWith("CUSTOM "), () -> "Message 
did not carry creator config: " + e.getMessage());
+       }
+
+       @Test void a02_noCreatorUsesDefaultMsg() {
+               var header = mock(RequestHeader.class);
+               when(header.asString()).thenReturn(Optional.of("actual"));
+
+               var h = new FluentRequestHeaderAssertion<>(header, null);
+
+               var e = assertThrows(RuntimeException.class, () -> 
h.is("expected"));
+               assertFalse(e.getMessage().startsWith("CUSTOM "));
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/swagger/SwaggerException_Test.java
 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/swagger/SwaggerException_Test.java
new file mode 100644
index 0000000000..fffce32af1
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/swagger/SwaggerException_Test.java
@@ -0,0 +1,51 @@
+/*
+ * 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.server.swagger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link SwaggerException} message formatting.
+ *
+ * <p>
+ * Regression: the constructor used to pre-format the location with {@code 
f(...)} and then pass the concatenated
+ * result to a printf constructor, giving a second format pass on which a 
surviving literal {@code '%'} could be
+ * re-interpreted as a format directive.  The message must now be formatted 
exactly once.
+ */
+class SwaggerException_Test extends TestBase {
+
+       @Test void a01_singleFormatPass() {
+               var ex = new SwaggerException(null, "Unexpected data type 
'%s'.", "Map");
+               assertEquals("Swagger exception:  at Unexpected data type 
'Map'.", ex.getMessage());
+       }
+
+       @Test void a02_literalPercentInArgSurvives() {
+               // A literal '%' inside an argument value must be substituted 
verbatim and must not throw.
+               var ex = new SwaggerException(null, "Unexpected data type 
'%s'.", "50% off");
+               assertEquals("Swagger exception:  at Unexpected data type '50% 
off'.", ex.getMessage());
+       }
+
+       @Test void a03_causePreserved() {
+               var cause = new IllegalStateException("nope");
+               var ex = new SwaggerException(cause, "In %s.", "location");
+               assertSame(cause, ex.getCause());
+               assertEquals("Swagger exception:  at In location.", 
ex.getMessage());
+       }
+}


Reply via email to