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 3e949b0867 Fix issue with @Path with default blank values
3e949b0867 is described below

commit 3e949b08671e63e57d22cd4016ef6476feaab99b
Author: James Bognar <[email protected]>
AuthorDate: Thu Nov 13 09:24:47 2025 -0500

    Fix issue with @Path with default blank values
---
 .../src/main/java/org/apache/juneau/Constants.java | 33 ++++++++++
 .../org/apache/juneau/http/annotation/Path.java    |  3 +-
 .../juneau/http/annotation/PathAnnotation.java     |  5 +-
 .../org/apache/juneau/httppart/HttpPartSchema.java |  8 ++-
 .../rest/client/remote/RemoteOperationMeta.java    |  7 +-
 .../java/org/apache/juneau/rest/arg/PathArg.java   |  4 +-
 .../juneau/httppart/HttpPartSchema_Path_Test.java  | 76 ++++++++++++++++++++++
 7 files changed, 127 insertions(+), 9 deletions(-)

diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Constants.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Constants.java
new file mode 100644
index 0000000000..d7d7badd2b
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Constants.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Global constants used across the Juneau framework.
+ */
+public class Constants {
+
+       /**
+        * Sentinel value to indicate that a default value is not specified.
+        * 
+        * <p>
+        * Used in annotation default values where empty string cannot 
distinguish between
+        * "no value specified" and "explicitly set to empty string".
+        */
+       public static final String NONE = "_NONE_";
+}
+
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
index 845bfb71b8..d2d9c22703 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
@@ -18,6 +18,7 @@ package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
+import static org.apache.juneau.Constants.*;
 
 import java.lang.annotation.*;
 
@@ -94,7 +95,7 @@ public @interface Path {
         *
         * @return The annotation value.
         */
-       String def() default "";
+       String def() default NONE;
 
        /**
         * Optional description for the exposed API.
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathAnnotation.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathAnnotation.java
index 31d37024e2..2ca5145fd6 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathAnnotation.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathAnnotation.java
@@ -18,6 +18,7 @@ package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
+import static org.apache.juneau.Constants.*;
 import static org.apache.juneau.common.utils.CollectionUtils.*;
 import static org.apache.juneau.common.utils.Utils.*;
 
@@ -89,7 +90,7 @@ public class PathAnnotation {
                Class<? extends HttpPartParser> parser = 
HttpPartParser.Void.class;
                Class<? extends HttpPartSerializer> serializer = 
HttpPartSerializer.Void.class;
                Schema schema = SchemaAnnotation.DEFAULT;
-               String name = "", value = "", def = "";
+               String name = "", value = "", def = NONE;
 
                /**
                 * Constructor.
@@ -274,7 +275,7 @@ public class PathAnnotation {
         */
        public static Value<String> findDef(ParameterInfo pi) {
                Value<String> n = Value.empty();
-               
rstream(pi.getAllAnnotationInfos(Path.class)).map(AnnotationInfo::inner).filter(x
 -> isNotEmpty(x.def())).forEach(x -> n.set(x.def()));
+               
rstream(pi.getAllAnnotationInfos(Path.class)).map(AnnotationInfo::inner).filter(x
 -> isNotEmpty(x.def()) && ne(NONE, x.def())).forEach(x -> n.set(x.def()));
                return n;
        }
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
index f6205c8d9e..6426435041 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
@@ -24,6 +24,7 @@ import static org.apache.juneau.common.utils.StringUtils.*;
 import static org.apache.juneau.common.utils.ThrowableUtils.*;
 import static org.apache.juneau.common.utils.Utils.*;
 import static org.apache.juneau.common.utils.Utils.isEmpty;
+import static org.apache.juneau.Constants.*;
 import static org.apache.juneau.httppart.HttpPartDataType.*;
 import static org.apache.juneau.httppart.HttpPartFormat.*;
 
@@ -2666,7 +2667,9 @@ public class HttpPartSchema {
                        if (! SchemaAnnotation.empty(a.schema()))
                                apply(a.schema());
                        name(firstNonEmpty(a.name(), a.value()));
-                       _default(a.def());
+                       String def = a.def();
+                       if (ne(NONE, def))
+                               _default = def;  // Set directly to allow empty 
strings as valid defaults
                        parser(a.parser());
                        serializer(a.serializer());
 
@@ -2675,7 +2678,8 @@ public class HttpPartSchema {
                                allowEmptyValue();
                                required(false);
                        } else if (required == null) {
-                               required(true);
+                               // Path parameters with default values are not 
required
+                               required(eq(NONE, def));
                        }
 
                        return this;
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteOperationMeta.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteOperationMeta.java
index dd95e37a74..82d3ab0657 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteOperationMeta.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteOperationMeta.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.rest.client.remote;
 
+import static org.apache.juneau.Constants.*;
 import static org.apache.juneau.common.utils.CollectionUtils.*;
 import static org.apache.juneau.common.utils.PredicateUtils.*;
 import static org.apache.juneau.common.utils.StringUtils.*;
@@ -233,7 +234,7 @@ public class RemoteOperationMeta {
                                .forEach(p -> {
                                        String name = firstNonEmpty(p.name(), 
p.value());
                                        String def = p.def();
-                                       if (isNotEmpty(name) && 
isNotEmpty(def)) {
+                                       if (isNotEmpty(name) && ne(NONE, def)) {
                                                defaults.put(name, def);
                                        }
                                });
@@ -245,13 +246,13 @@ public class RemoteOperationMeta {
                                        for (var p : x.value()) {
                                                String name = 
firstNonEmpty(p.name(), p.value());
                                                String def = p.def();
-                                               if (isNotEmpty(name) && 
isNotEmpty(def)) {
+                                               if (isNotEmpty(name) && 
ne(NONE, def)) {
                                                        defaults.put(name, def);
                                                }
                                        }
                                });
                }
-       
+
                private static void processQueryDefaults(MethodInfo mi, 
Map<String,String> defaults) {
                        rstream(mi.getAllAnnotationInfos())
                                .map(x -> x.cast(Query.class))
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/PathArg.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/PathArg.java
index 094a0fd848..4c2f76262f 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/PathArg.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/PathArg.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.rest.arg;
 
+import static org.apache.juneau.Constants.*;
 import static org.apache.juneau.common.utils.StringUtils.*;
 import static org.apache.juneau.common.utils.Utils.*;
 import static org.apache.juneau.http.annotation.PathAnnotation.*;
@@ -172,7 +173,8 @@ public class PathArg implements RestOpArg {
                Path mergedPath = getMergedPath(paramInfo, name);
 
                // Use merged path annotation for all lookups
-               this.def = nn(mergedPath) && ! mergedPath.def().isEmpty() ? 
mergedPath.def() : findDef(paramInfo).orElse(null);
+               String pathDef = nn(mergedPath) ? mergedPath.def() : null;
+               this.def = nn(pathDef) && ne(NONE, pathDef) ? pathDef : 
findDef(paramInfo).orElse(null);
                this.type = paramInfo.getParameterType().innerType();
                this.schema = nn(mergedPath) ? 
HttpPartSchema.create(mergedPath) : HttpPartSchema.create(Path.class, 
paramInfo);
                Class<? extends HttpPartParser> pp = schema.getParser();
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/httppart/HttpPartSchema_Path_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/httppart/HttpPartSchema_Path_Test.java
index 599b2445f8..205fbf5f3b 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/httppart/HttpPartSchema_Path_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/httppart/HttpPartSchema_Path_Test.java
@@ -814,4 +814,80 @@ class HttpPartSchema_Path_Test extends TestBase {
                assertThrowsWithMessage(SchemaValidationException.class, 
"Maximum number of items exceeded.", 
()->s.getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), 
BeanContext.DEFAULT));
                assertThrowsWithMessage(SchemaValidationException.class, 
"Maximum number of items exceeded.", 
()->s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5,6"),
 BeanContext.DEFAULT));
        }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // @Path with default values - required flag behavior
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Path(name="x")
+       public static class E01 {}
+
+       @Test void e01_required_noDefault() {
+               var s = HttpPartSchema.create().applyAll(Path.class, 
E01.class).build();
+               // Path parameters without default values should be required
+               assertTrue(s.isRequired());
+       }
+
+       @Path(name="x", def="defaultValue")
+       public static class E02 {}
+
+       @Test void e02_notRequired_withDefault() {
+               var s = HttpPartSchema.create().applyAll(Path.class, 
E02.class).build();
+               // Path parameters with default values should NOT be required
+               assertFalse(s.isRequired());
+               assertEquals("defaultValue", s.getDefault());
+       }
+
+       @Path(name="x", def="")
+       public static class E03 {}
+
+       @Test void e03_notRequired_withEmptyDefault() {
+               var s = HttpPartSchema.create().applyAll(Path.class, 
E03.class).build();
+               // Path parameters with empty string default should NOT be 
required
+               assertFalse(s.isRequired());
+               assertEquals("", s.getDefault());
+       }
+
+       public static class E04 {
+               public void a(
+                               @Path(name = "logger", def = "") String 
loggerName,
+                               @Path(name = "level", def = "INFO") String 
levelName
+                       ) {
+                       /* no-op */
+               }
+       }
+
+       @Test void e04_multiplePathParamsWithDefaults() throws Exception {
+               var loggerParam = MethodInfo.of(E04.class.getMethod("a", 
String.class, String.class)).getParameter(0);
+               var levelParam = MethodInfo.of(E04.class.getMethod("a", 
String.class, String.class)).getParameter(1);
+
+               var loggerSchema = HttpPartSchema.create().applyAll(Path.class, 
loggerParam).build();
+               var levelSchema = HttpPartSchema.create().applyAll(Path.class, 
levelParam).build();
+
+               // Both should not be required since they have defaults
+               assertFalse(loggerSchema.isRequired());
+               assertFalse(levelSchema.isRequired());
+
+               // Verify defaults are set correctly
+               assertEquals("", loggerSchema.getDefault());
+               assertEquals("INFO", levelSchema.getDefault());
+
+               // Verify names are set correctly
+               assertEquals("logger", loggerSchema.getName());
+               assertEquals("level", levelSchema.getName());
+
+               // This should not throw the regression error:
+               // "Cannot specify a default value on a required value."
+               assertDoesNotThrow(() -> loggerSchema.validateInput("test"));
+               assertDoesNotThrow(() -> levelSchema.validateInput("DEBUG"));
+       }
+
+       @Path(value="/remainder")
+       public static class E05 {}
+
+       @Test void e05_pathRemainder_notRequired() {
+               var s = HttpPartSchema.create().applyAll(Path.class, 
E05.class).build();
+               // Path remainders (starting with '/') should never be required
+               assertFalse(s.isRequired());
+       }
 }
\ No newline at end of file

Reply via email to