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?