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 70cc367a21 Fix issue around navlink generation around bare schema-only 
lines
70cc367a21 is described below

commit 70cc367a2159287f9f007d0f40f4e3e5f8a731ec
Author: James Bognar <[email protected]>
AuthorDate: Fri Mar 20 10:01:01 2026 -0400

    Fix issue around navlink generation around bare schema-only lines
---
 .../src/main/java/org/apache/juneau/BeanMeta.java  | 25 ++++++
 .../java/org/apache/juneau/annotation/Beanp.java   |  5 ++
 .../apache/juneau/html/BasicHtmlDocTemplate.java   | 17 +++++
 .../annotation/Beanp_StarPlainField_Test.java      | 89 ++++++++++++++++++++++
 ...sicHtmlDocTemplate_Navlink_RequestUri_Test.java | 57 ++++++++++++++
 todo/TODO.md                                       |  5 +-
 6 files changed, 194 insertions(+), 4 deletions(-)

diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
index 72fcd484f2..4e5bbbc232 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
@@ -454,6 +454,7 @@ public class BeanMeta<T> {
                                                .filter(Objects::nonNull)
                                                .findFirst()
                                                
.orElse(propertyNamer.getPropertyName(x.getName()));
+                                       name = resolveBeanFieldPropertyName(x, 
name, propertyNamer);
                                        if (nn(name)) {
                                                
normalProps.computeIfAbsent(name, n->BeanPropertyMeta.builder(this, 
n)).setField(x);
                                        }
@@ -1025,6 +1026,29 @@ public class BeanMeta<T> {
                return new BeanConstructor(opte(), liste());
        }
 
+       /**
+        * Resolves the bean property name for a field when {@link Beanp 
@Beanp} or {@link Name @Name} supplies
+        * <js>"*"</js>.
+        *
+        * <p>
+        * On a {@link Map} field, <js>"*"</js> remains the dyna-property name. 
On any other field type, <js>"*"</js> is
+        * treated like an unnamed {@code @Beanp} (apply other attributes) but 
the property name is taken from the field
+        * (via {@link PropertyNamer#getPropertyName(String)}).
+        * </p>
+        *
+        * @param x Field being registered.
+        * @param nameFromAnnotations Name from {@code @Beanp}/{@code @Name}, 
or already-resolved default from the field name.
+        * @param propertyNamer Namer for raw field names.
+        * @return Property key to use in {@link BeanMeta}.
+        */
+       private static String resolveBeanFieldPropertyName(FieldInfo x, String 
nameFromAnnotations, PropertyNamer propertyNamer) {
+               if (! "*".equals(nameFromAnnotations))
+                       return nameFromAnnotations;
+               if (x.getFieldType().isAssignableTo(Map.class))
+                       return "*";
+               return propertyNamer.getPropertyName(x.getName());
+       }
+
        /*
         * Finds all bean fields in the class hierarchy.
         *
@@ -1430,6 +1454,7 @@ public class BeanMeta<T> {
                                        .filter(Objects::nonNull)
                                        .findFirst()
                                        
.orElse(propertyNamer.getPropertyName(x.getName()));
+                               name = resolveBeanFieldPropertyName(x, name, 
propertyNamer);
                                if (nn(name))
                                        s.add(name);
                        }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanp.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanp.java
index 6b7b46a576..34d954642c 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanp.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanp.java
@@ -127,6 +127,11 @@ public @interface Beanp {
         *
         * <h5 class='topic'>Dynamic beans</h5>
         * <p>
+        * On a <strong>field</strong> whose type is not a {@link Map}, 
<js>"*"</js> does not create a dynamic property:
+        * the property name is the field name (same as omitting {@code 
name}/{@code value}), while other attributes on
+        * this annotation (e.g. {@link #format()}, {@link #swap()}) still 
apply.
+        * </p>
+        * <p>
         * The bean property named <js>"*"</js> is the designated "dynamic 
property" which allows for "extra" bean
         * properties not otherwise defined.
         * This is similar in concept to the Jackson <ja>@JsonGetterAll</ja> 
and <ja>@JsonSetterAll</ja> annotations.
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/BasicHtmlDocTemplate.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/BasicHtmlDocTemplate.java
index 5d3b194def..c55e02d3f9 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/BasicHtmlDocTemplate.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/BasicHtmlDocTemplate.java
@@ -21,6 +21,7 @@ import static org.apache.juneau.commons.utils.Utils.*;
 import static org.apache.juneau.html.AsideFloat.*;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.juneau.commons.lang.*;
@@ -39,6 +40,7 @@ import org.apache.juneau.commons.utils.*;
 @SuppressWarnings({
        "resource", // Resource management handled externally
        "java:S112", // Generic exception thrown; template methods throw 
Exception for subclass flexibility
+       "java:S1192", // HTML tag names (e.g. aside, section) repeated in 
template; constants would obscure markup
 })
 public class BasicHtmlDocTemplate implements HtmlDocTemplate {
 
@@ -58,6 +60,9 @@ public class BasicHtmlDocTemplate implements HtmlDocTemplate {
                return o.toString().isEmpty();
        }
 
+       /** Lowercase marshall URI schemes recognized in bare <code>key: 
href</code> navlinks in {@link #nav}. */
+       private static final List<String> MARSHALL_URI_NAVLINK_SCHEMES = 
List.of("request", "servlet", "context");
+
        @Override /* Overridden from HtmlDocTemplate */
        public void writeTo(HtmlDocSerializerSession session, HtmlWriter w, 
Object o) throws Exception {
                w.sTag("html").nl(0);
@@ -331,6 +336,18 @@ public class BasicHtmlDocTemplate implements 
HtmlDocTemplate {
                                        var i = l.indexOf(':');
                                        var key = l.substring(0, i);
                                        var val = l.substring(i + 1).trim();
+                                       // Bare "request:/?q", "request:?q", 
"servlet:...", "context:..." were split so key was the scheme
+                                       // and val lost the scheme; resolveUri 
then saw /? or ? only and browsers turned "request:?q" into a
+                                       // bogus path segment. Rejoin to a full 
marshall URI when val is path/query/fragment only.
+                                       var scheme = 
MARSHALL_URI_NAVLINK_SCHEMES.stream()
+                                               .filter(sch -> 
sch.equalsIgnoreCase(key))
+                                               .findFirst()
+                                               .orElse(null);
+                                       if (scheme != null && ! val.isEmpty()) {
+                                               var c0 = val.charAt(0);
+                                               if (c0 == '/' || c0 == '?' || 
c0 == '#')
+                                                       val = scheme + ':' + 
val;
+                                       }
                                        if (val.startsWith("<"))
                                                w.nl(4).appendln(5, val);
                                        else
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/annotation/Beanp_StarPlainField_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/annotation/Beanp_StarPlainField_Test.java
new file mode 100644
index 0000000000..5627d1f7eb
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/annotation/Beanp_StarPlainField_Test.java
@@ -0,0 +1,89 @@
+/*
+ * 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.annotation;
+
+import static org.apache.juneau.TestUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.Test;
+
+/**
+ * {@link Beanp @Beanp} with name/value {@code "*"} on non-{@link Map} fields 
exposes a normal property named after the
+ * field; {@code Map} fields keep the dyna property {@code *}.
+ */
+class Beanp_StarPlainField_Test extends TestBase {
+
+       /** {@code *} via shorthand {@link Beanp#value()}. */
+       public static class A {
+               @Beanp("*")
+               public String taggedValue;
+       }
+
+       /** {@code *} via {@link Beanp#name()}. */
+       public static class B {
+               @Beanp(name = "*")
+               public int count;
+       }
+
+       /** Dynamic {@code Map} field must remain the dyna property {@code *}. 
*/
+       public static class C {
+               @Beanp("*")
+               public Map<String, Object> extra = new LinkedHashMap<>();
+       }
+
+       @Test
+       void a01_beanpStarOnStringField_isPropertyNamedAfterField() {
+               var cm = BeanContext.DEFAULT.getClassMeta(A.class);
+               assertTrue(cm.isBean(), cm.getNotABeanReason());
+               var bm = cm.getBeanMeta();
+               assertNotNull(bm);
+               var props = bm.getProperties();
+               assertTrue(props.containsKey("taggedValue"), () -> "keys=" + 
props.keySet());
+               assertFalse(props.get("taggedValue").isDyna());
+               var a = new A();
+               a.taggedValue = "x";
+               assertEquals("x", jsonRoundTrip(a, A.class).taggedValue);
+       }
+
+       @Test
+       void a02_beanpNameStarOnPrimitiveField_isPropertyNamedAfterField() {
+               var cm = BeanContext.DEFAULT.getClassMeta(B.class);
+               assertTrue(cm.isBean(), cm.getNotABeanReason());
+               var bm = cm.getBeanMeta();
+               assertNotNull(bm);
+               var props = bm.getProperties();
+               assertTrue(props.containsKey("count"), () -> "keys=" + 
props.keySet());
+               assertFalse(props.get("count").isDyna());
+               var b = new B();
+               b.count = 7;
+               assertEquals(7, jsonRoundTrip(b, B.class).count);
+       }
+
+       @Test
+       void b01_beanpStarOnMapField_staysDynaProperty() {
+               var cm = BeanContext.DEFAULT.getClassMeta(C.class);
+               assertTrue(cm.isBean(), cm.getNotABeanReason());
+               var bm = cm.getBeanMeta();
+               assertNotNull(bm);
+               var props = bm.getProperties();
+               assertTrue(props.containsKey("*"), () -> "keys=" + 
props.keySet());
+               assertTrue(props.get("*").isDyna());
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/html/BasicHtmlDocTemplate_Navlink_RequestUri_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/html/BasicHtmlDocTemplate_Navlink_RequestUri_Test.java
new file mode 100644
index 0000000000..1452db9f9f
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/html/BasicHtmlDocTemplate_Navlink_RequestUri_Test.java
@@ -0,0 +1,57 @@
+/*
+ * 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.html;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.html.annotation.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.config.*;
+import org.apache.juneau.rest.mock.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies {@link BasicHtmlDocTemplate} navlink parsing for bare {@code 
request:/?query} and {@code request:?query}.
+ */
+@SuppressWarnings("serial")
+class BasicHtmlDocTemplate_Navlink_RequestUri_Test extends TestBase {
+
+       @Rest
+       @HtmlDocConfig(navlinks = {
+               "q: request:?Accept=text/json&plainText=true",
+               "request:?Accept=text/plain&plainText=true",
+               "request:/?Accept=text/html&plainText=true"
+       })
+       public static class R extends BasicRestServlet implements 
BasicJsonHtmlConfig {
+               @RestGet
+               public String page() {
+                       return "OK";
+               }
+       }
+
+       @Test
+       void a01_bareRequestQueryNavlinksResolveWithoutRequestPathSegment() 
throws Exception {
+               var content = 
MockRestClient.buildLax(R.class).get("/page").accept("text/html").run().getContent().asString();
+               assertFalse(content.contains("href=\"request:"),
+                       "href must not be browser-relative request:?... 
(creates .../request:?...)");
+               assertTrue(content.contains("text/json") || 
content.contains("text%2Fjson"), content);
+               assertTrue(content.contains("text/plain") || 
content.contains("text%2Fplain"), content);
+               assertTrue(content.contains("text/html") || 
content.contains("text%2Fhtml"), content);
+       }
+}
diff --git a/todo/TODO.md b/todo/TODO.md
index 657cbb31f6..a1aba2c607 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -1,11 +1,8 @@
 # TODO
 
 
-- Investigate navlinks URL generation issue: Either 
"request:?Accept=text/json&plainText=true" should be supported, or 
"request:/?Accept=text/json&plainText=true" should not append '/' to the 
request URL. Currently, "request:/?Accept=..." generates URLs like 
"http://localhost:5000/rest/db/request:?Accept=..."; which is incorrect.
 - Update REST server API to use new BeanStore2.
-- Make sure @Beanp("*") works on plain fields.
-- Need an easier way to specify this header:
-    Content-Disposition: attachment; filename="example.pdf"
+- Need an easier way to specify this header:  Content-Disposition: attachment; 
filename="example.pdf"
 
 - A comprehensive plan for handling large data sets using Suppliers and 
Consumers?
 

Reply via email to