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 c77fcc67c1 JUNEAU-225
c77fcc67c1 is described below

commit c77fcc67c17ce5af3ec1e25a0882710ad9de64e0
Author: James Bognar <[email protected]>
AuthorDate: Tue Oct 14 13:07:16 2025 -0400

    JUNEAU-225
---
 juneau-docs/docs/release-notes/9.2.0.md            |  59 +++++++++++
 .../org/apache/juneau/rest/annotation/Rest.java    | 108 +++++++++++++++++++++
 .../juneau/rest/annotation/RestAnnotation.java     |  33 +++++++
 .../org/apache/juneau/rest/arg/FormDataArg.java    |  88 ++++++++++++++++-
 .../java/org/apache/juneau/rest/arg/HeaderArg.java |  88 ++++++++++++++++-
 .../java/org/apache/juneau/rest/arg/PathArg.java   |  90 ++++++++++++++++-
 .../java/org/apache/juneau/rest/arg/QueryArg.java  |  88 ++++++++++++++++-
 .../apache/juneau/rest/annotation/Query_Test.java  |  93 ++++++++++++++++++
 8 files changed, 639 insertions(+), 8 deletions(-)

diff --git a/juneau-docs/docs/release-notes/9.2.0.md 
b/juneau-docs/docs/release-notes/9.2.0.md
index f02d6dae9e..c3cac07fee 100644
--- a/juneau-docs/docs/release-notes/9.2.0.md
+++ b/juneau-docs/docs/release-notes/9.2.0.md
@@ -648,6 +648,65 @@ The previous `juneau-all` module has been deprecated and 
removed in favor of `ju
 
 - **Behavior**: Default values are only applied when the parameter value is 
`null`. Empty strings, zero values, and empty collections are considered valid 
values and will not trigger the default.
 
+### juneau-rest-server
+
+#### Class-Level HTTP Parameter Defaults
+
+- **`@Rest` Parameter Arrays**: Added support for defining default HTTP 
parameter values at the REST class level that apply to all methods in the 
resource.
+  - **`queryParams={...}`**: Define default query parameter values
+  - **`headerParams={...}`**: Define default header parameter values
+  - **`pathParams={...}`**: Define default path parameter values
+  - **`formDataParams={...}`**: Define default form data parameter values
+  
+  Key features:
+  - Eliminates duplication of common parameters across multiple methods
+  - Class-level defaults are merged with method-level parameter annotations
+  - Method-level values take precedence over class-level defaults
+  - Affects validation, parsing, and OpenAPI/Swagger documentation generation
+  
+  Example usage:
+  ```java
+  @Rest(
+      queryParams={
+          @Query(name="format", def="json", description="Output format 
(json|xml)"),
+          @Query(name="verbose", def="false", schema=@Schema(type="boolean", 
description="Include verbose output"))
+      },
+      headerParams={
+          @Header(name="X-API-Version", def="1.0", description="API version")
+      },
+      formDataParams={
+          @FormData(name="action", def="submit", description="Form action")
+      }
+  )
+  public class MyRestResource {
+  
+      @RestGet("/data")
+      public Data getData(
+          @Query("format") String format,        // Inherits def="json" and 
description
+          @Query("verbose") boolean verbose,     // Inherits def="false" and 
schema
+          @Header("X-API-Version") String version  // Inherits def="1.0" and 
description
+      ) {
+          return formatData(data, format, verbose, version);
+      }
+  
+      @RestPost("/update")
+      public String updateData(
+          @Query(name="format", def="xml") String format,  // Overrides 
class-level def
+          @Query("verbose") boolean verbose,               // Still uses 
class-level def="false"
+          @FormData("action") String action                // Inherits 
def="submit" and description
+      ) {
+          return "Updated in " + format + " format with action: " + action;
+      }
+  }
+  ```
+  
+  Benefits:
+  - **DRY Principle**: Define common parameters once at the class level
+  - **Maintainability**: Update defaults in one place instead of across 
multiple methods
+  - **Consistency**: Ensures uniform parameter behavior across the REST API
+  - **Documentation**: Class-level defaults are reflected in generated 
Swagger/OpenAPI specifications
+  - **Validation**: Schema validation from class-level definitions applies to 
all methods
+
 ### Documentation
 
 #### Historical Javadocs
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Rest.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Rest.java
index 04fdc5d67e..0f88da3980 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Rest.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Rest.java
@@ -28,6 +28,7 @@ import org.apache.juneau.annotation.*;
 import org.apache.juneau.config.*;
 import org.apache.juneau.cp.*;
 import org.apache.juneau.encoders.*;
+import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.rest.*;
@@ -1439,4 +1440,111 @@ public @interface Rest {
         * @return The annotation value.
         */
        String uriResolution() default "";
+
+       /**
+        * Default query parameter definitions.
+        *
+        * <p>
+        * Provides default values for {@link Query @Query} annotations on 
method parameters across all methods in this class.
+        * Values specified here are used as defaults when the same property is 
not explicitly defined on a method parameter's
+        * {@link Query @Query} annotation.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <jc>// Define common query parameters at class level</jc>
+        *      <ja>@Rest</ja>(
+        *              queryParams={
+        *                      <ja>@Query</ja>(name=<js>"format"</js>, 
def=<js>"json"</js>, description=<js>"Output format"</js>),
+        *                      <ja>@Query</ja>(name=<js>"verbose"</js>, 
def=<js>"false"</js>, schema=<ja>@Schema</ja>(type=<js>"boolean"</js>))
+        *              }
+        *      )
+        *      <jk>public class</jk> MyResource {
+        *
+        *              <jc>// Method will inherit the default query parameter 
definitions</jc>
+        *              <ja>@RestGet</ja>
+        *              <jk>public</jk> String 
doGet(<ja>@Query</ja>(<js>"format"</js>) String format, 
<ja>@Query</ja>(<js>"verbose"</js>) <jk>boolean</jk> verbose) {...}
+        *
+        *              <jc>// Can override defaults on a per-method basis</jc>
+        *              <ja>@RestGet</ja>
+        *              <jk>public</jk> String 
doGet2(<ja>@Query</ja>(name=<js>"format"</js>, def=<js>"xml"</js>) String 
format) {...}
+        *      }
+        * </p>
+        *
+        * <h5 class='section'>Notes:</h5><ul>
+        *      <li class='note'>
+        *              These defaults apply to validation, parsing, and 
OpenAPI/Swagger documentation generation.
+        *      <li class='note'>
+        *              Method-level {@link Query @Query} annotations take 
precedence over class-level definitions.
+        *      <li class='note'>
+        *              The {@link Query#name() name} attribute must be 
specified for each query definition.
+        * </ul>
+        *
+        * @return The annotation value.
+        * @since 9.2.0
+        */
+       Query[] queryParams() default {};
+
+       /**
+        * Default header parameter definitions.
+        *
+        * <p>
+        * Provides default values for {@link Header @Header} annotations on 
method parameters across all methods in this class.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <ja>@Rest</ja>(
+        *              headerParams={
+        *                      
<ja>@Header</ja>(name=<js>"Accept-Language"</js>, def=<js>"en-US"</js>),
+        *                      <ja>@Header</ja>(name=<js>"X-API-Version"</js>, 
def=<js>"1.0"</js>)
+        *              }
+        *      )
+        *      <jk>public class</jk> MyResource {...}
+        * </p>
+        *
+        * @return The annotation value.
+        * @since 9.2.0
+        */
+       Header[] headerParams() default {};
+
+       /**
+        * Default path parameter definitions.
+        *
+        * <p>
+        * Provides default values for {@link Path @Path} annotations on method 
parameters across all methods in this class.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <ja>@Rest</ja>(
+        *              pathParams={
+        *                      <ja>@Path</ja>(name=<js>"version"</js>, 
def=<js>"v1"</js>)
+        *              }
+        *      )
+        *      <jk>public class</jk> MyResource {...}
+        * </p>
+        *
+        * @return The annotation value.
+        * @since 9.2.0
+        */
+       Path[] pathParams() default {};
+
+       /**
+        * Default form data parameter definitions.
+        *
+        * <p>
+        * Provides default values for {@link FormData @FormData} annotations 
on method parameters across all methods in this class.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <ja>@Rest</ja>(
+        *              formDataParams={
+        *                      <ja>@FormData</ja>(name=<js>"action"</js>, 
def=<js>"submit"</js>)
+        *              }
+        *      )
+        *      <jk>public class</jk> MyResource {...}
+        * </p>
+        *
+        * @return The annotation value.
+        * @since 9.2.0
+        */
+       FormData[] formDataParams() default {};
 }
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestAnnotation.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestAnnotation.java
index 43b1716431..a890b8a9cc 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestAnnotation.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestAnnotation.java
@@ -26,6 +26,7 @@ import org.apache.juneau.annotation.*;
 import org.apache.juneau.cp.*;
 import org.apache.juneau.encoders.*;
 import org.apache.juneau.http.*;
+import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.reflect.*;
 import org.apache.juneau.rest.*;
@@ -119,6 +120,10 @@ public class RestAnnotation {
                Swagger swagger = SwaggerAnnotation.DEFAULT;
                String disableContentParam="", allowedHeaderParams="", 
allowedMethodHeaders="", allowedMethodParams="", clientVersionHeader="", 
config="", debug="", debugOn="", defaultAccept="", defaultCharset="", 
defaultContentType="", maxInput="", messages="", path="", 
renderResponseStackTraces="", roleGuard="", rolesDeclared="", siteName="", 
uriAuthority="", uriContext="", uriRelativity="", uriResolution="";
                String[] consumes={}, defaultRequestAttributes={}, 
defaultRequestHeaders={}, defaultResponseHeaders={}, produces={}, title={};
+               Query[] queryParams = new Query[0];
+               Header[] headerParams = new Header[0];
+               Path[] pathParams = new Path[0];
+               FormData[] formDataParams = new FormData[0];
 
                /**
                 * Constructor.
@@ -675,6 +680,10 @@ public class RestAnnotation {
                private final Swagger swagger;
                private final String disableContentParam, allowedHeaderParams, 
allowedMethodHeaders, allowedMethodParams, clientVersionHeader, config, debug, 
debugOn, defaultAccept, defaultCharset, defaultContentType, maxInput, messages, 
path, renderResponseStackTraces, roleGuard, rolesDeclared, siteName, 
uriAuthority, uriContext, uriRelativity, uriResolution;
                private final String[] consumes, produces, 
defaultRequestAttributes, defaultRequestHeaders, defaultResponserHeaders, title;
+               private final Query[] queryParams;
+               private final Header[] headerParams;
+               private final Path[] pathParams;
+               private final FormData[] formDataParams;
 
                Impl(Builder b) {
                        super(b);
@@ -724,6 +733,10 @@ public class RestAnnotation {
                        this.uriContext = b.uriContext;
                        this.uriRelativity = b.uriRelativity;
                        this.uriResolution = b.uriResolution;
+                       this.queryParams = copyOf(b.queryParams);
+                       this.headerParams = copyOf(b.headerParams);
+                       this.pathParams = copyOf(b.pathParams);
+                       this.formDataParams = copyOf(b.formDataParams);
                        postConstruct();
                }
 
@@ -956,6 +969,26 @@ public class RestAnnotation {
                public String uriResolution() {
                        return uriResolution;
                }
+
+               @Override /* Rest */
+               public Query[] queryParams() {
+                       return queryParams;
+               }
+
+               @Override /* Rest */
+               public Header[] headerParams() {
+                       return headerParams;
+               }
+
+               @Override /* Rest */
+               public Path[] pathParams() {
+                       return pathParams;
+               }
+
+               @Override /* Rest */
+               public FormData[] formDataParams() {
+                       return formDataParams;
+               }
        }
 
        
//-----------------------------------------------------------------------------------------------------------------
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/FormDataArg.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/FormDataArg.java
index c73719cb71..dcae491015 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/FormDataArg.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/FormDataArg.java
@@ -83,10 +83,16 @@ public class FormDataArg implements RestOpArg {
         * @param annotations The annotations to apply to any new part parsers.
         */
        protected FormDataArg(ParamInfo pi, AnnotationWorkList annotations) {
+               // Get the form data parameter name
                this.name = findName(pi).orElseThrow(()->new ArgException(pi, 
"@FormData used without name or value"));
-               this.def = findDef(pi).orElse(null);
+
+               // Check for class-level defaults and merge if found
+               FormData mergedFormData = getMergedFormData(pi, name);
+
+               // Use merged form data annotation for all lookups
+               this.def = mergedFormData != null && 
!mergedFormData.def().isEmpty() ? mergedFormData.def() : 
findDef(pi).orElse(null);
                this.type = pi.getParameterType();
-               this.schema = HttpPartSchema.create(FormData.class, pi);
+               this.schema = mergedFormData != null ? 
HttpPartSchema.create(mergedFormData) : HttpPartSchema.create(FormData.class, 
pi);
                Class<? extends HttpPartParser> pp = schema.getParser();
                this.partParser = pp != null ? 
HttpPartParser.creator().type(pp).apply(annotations).create() : null;
                this.multi = schema.getCollectionFormat() == 
HttpPartCollectionFormat.MULTI;
@@ -95,6 +101,84 @@ public class FormDataArg implements RestOpArg {
                        throw new ArgException(pi, "Use of multipart flag on 
@FormData parameter that is not an array or Collection");
        }
 
+       /**
+        * Gets the merged @FormData annotation combining class-level and 
parameter-level values.
+        *
+        * @param pi The parameter info.
+        * @param paramName The form data parameter name.
+        * @return Merged annotation, or null if no class-level defaults exist.
+        */
+       private static FormData getMergedFormData(ParamInfo pi, String 
paramName) {
+               // Get the declaring class
+               ClassInfo declaringClass = pi.getMethod().getDeclaringClass();
+               if (declaringClass == null)
+                       return null;
+
+               // Find @Rest annotation on the class
+               Rest restAnnotation = declaringClass.getAnnotation(Rest.class);
+               if (restAnnotation == null)
+                       return null;
+
+               // Find matching @FormData from class-level formDataParams array
+               FormData classLevelFormData = null;
+               for (FormData f : restAnnotation.formDataParams()) {
+                       String fName = firstNonEmpty(f.name(), f.value());
+                       if (paramName.equals(fName)) {
+                               classLevelFormData = f;
+                               break;
+                       }
+               }
+
+               if (classLevelFormData == null)
+                       return null;
+
+               // Get parameter-level @FormData
+               FormData paramFormData = pi.getAnnotation(FormData.class);
+               if (paramFormData == null)
+                       paramFormData = 
pi.getParameterType().getAnnotation(FormData.class);
+
+               if (paramFormData == null) {
+                       // No parameter-level @FormData, use class-level as-is
+                       return classLevelFormData;
+               }
+
+               // Merge the two annotations: parameter-level takes precedence
+               return mergeAnnotations(classLevelFormData, paramFormData);
+       }
+
+       /**
+        * Merges two @FormData annotations, with param-level taking precedence 
over class-level.
+        *
+        * @param classLevel The class-level default.
+        * @param paramLevel The parameter-level override.
+        * @return Merged annotation.
+        */
+       private static FormData mergeAnnotations(FormData classLevel, FormData 
paramLevel) {
+               return FormDataAnnotation.create()
+                       .name(firstNonEmpty(paramLevel.name(), 
paramLevel.value(), classLevel.name(), classLevel.value()))
+                       .value(firstNonEmpty(paramLevel.value(), 
paramLevel.name(), classLevel.value(), classLevel.name()))
+                       .def(firstNonEmpty(paramLevel.def(), classLevel.def()))
+                       .description(paramLevel.description().length > 0 ? 
paramLevel.description() : classLevel.description())
+                       .parser(paramLevel.parser() != 
HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser())
+                       .serializer(paramLevel.serializer() != 
HttpPartSerializer.Void.class ? paramLevel.serializer() : 
classLevel.serializer())
+                       .schema(mergeSchemas(classLevel.schema(), 
paramLevel.schema()))
+                       .build();
+       }
+
+       /**
+        * Merges two @Schema annotations, with param-level taking precedence.
+        *
+        * @param classLevel The class-level default.
+        * @param paramLevel The parameter-level override.
+        * @return Merged annotation.
+        */
+       private static Schema mergeSchemas(Schema classLevel, Schema 
paramLevel) {
+               // If parameter has a non-default schema, use it; otherwise use 
class-level
+               if (!SchemaAnnotation.empty(paramLevel))
+                       return paramLevel;
+               return classLevel;
+       }
+
        @SuppressWarnings({ "rawtypes", "unchecked" })
        @Override /* RestOpArg */
        public Object resolve(RestOpSession opSession) throws Exception {
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/HeaderArg.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/HeaderArg.java
index d0abaf9ffa..35d3c63ff5 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/HeaderArg.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/HeaderArg.java
@@ -124,10 +124,16 @@ public class HeaderArg implements RestOpArg {
         * @param annotations The annotations to apply to any new part parsers.
         */
        protected HeaderArg(ParamInfo pi, AnnotationWorkList annotations) {
+               // Get the header name from the parameter
                this.name = findName(pi).orElseThrow(() -> new ArgException(pi, 
"@Header used without name or value"));
-               this.def = findDef(pi).orElse(null);
+
+               // Check for class-level defaults and merge if found
+               Header mergedHeader = getMergedHeader(pi, name);
+
+               // Use merged header annotation for all lookups
+               this.def = mergedHeader != null && 
!mergedHeader.def().isEmpty() ? mergedHeader.def() : findDef(pi).orElse(null);
                this.type = pi.getParameterType();
-               this.schema = HttpPartSchema.create(Header.class, pi);
+               this.schema = mergedHeader != null ? 
HttpPartSchema.create(mergedHeader) : HttpPartSchema.create(Header.class, pi);
                Class<? extends HttpPartParser> pp = schema.getParser();
                this.partParser = pp != null ? 
HttpPartParser.creator().type(pp).apply(annotations).create() : null;
                this.multi = schema.getCollectionFormat() == 
HttpPartCollectionFormat.MULTI;
@@ -136,6 +142,84 @@ public class HeaderArg implements RestOpArg {
                        throw new ArgException(pi, "Use of multipart flag on 
@Header parameter that is not an array or Collection");
        }
 
+       /**
+        * Gets the merged @Header annotation combining class-level and 
parameter-level values.
+        *
+        * @param pi The parameter info.
+        * @param paramName The header name.
+        * @return Merged annotation, or null if no class-level defaults exist.
+        */
+       private static Header getMergedHeader(ParamInfo pi, String paramName) {
+               // Get the declaring class
+               ClassInfo declaringClass = pi.getMethod().getDeclaringClass();
+               if (declaringClass == null)
+                       return null;
+
+               // Find @Rest annotation on the class
+               Rest restAnnotation = declaringClass.getAnnotation(Rest.class);
+               if (restAnnotation == null)
+                       return null;
+
+               // Find matching @Header from class-level headerParams array
+               Header classLevelHeader = null;
+               for (Header h : restAnnotation.headerParams()) {
+                       String hName = firstNonEmpty(h.name(), h.value());
+                       if (paramName.equals(hName)) {
+                               classLevelHeader = h;
+                               break;
+                       }
+               }
+
+               if (classLevelHeader == null)
+                       return null;
+
+               // Get parameter-level @Header
+               Header paramHeader = pi.getAnnotation(Header.class);
+               if (paramHeader == null)
+                       paramHeader = 
pi.getParameterType().getAnnotation(Header.class);
+
+               if (paramHeader == null) {
+                       // No parameter-level @Header, use class-level as-is
+                       return classLevelHeader;
+               }
+
+               // Merge the two annotations: parameter-level takes precedence
+               return mergeAnnotations(classLevelHeader, paramHeader);
+       }
+
+       /**
+        * Merges two @Header annotations, with param-level taking precedence 
over class-level.
+        *
+        * @param classLevel The class-level default.
+        * @param paramLevel The parameter-level override.
+        * @return Merged annotation.
+        */
+       private static Header mergeAnnotations(Header classLevel, Header 
paramLevel) {
+               return HeaderAnnotation.create()
+                       .name(firstNonEmpty(paramLevel.name(), 
paramLevel.value(), classLevel.name(), classLevel.value()))
+                       .value(firstNonEmpty(paramLevel.value(), 
paramLevel.name(), classLevel.value(), classLevel.name()))
+                       .def(firstNonEmpty(paramLevel.def(), classLevel.def()))
+                       .description(paramLevel.description().length > 0 ? 
paramLevel.description() : classLevel.description())
+                       .parser(paramLevel.parser() != 
HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser())
+                       .serializer(paramLevel.serializer() != 
HttpPartSerializer.Void.class ? paramLevel.serializer() : 
classLevel.serializer())
+                       .schema(mergeSchemas(classLevel.schema(), 
paramLevel.schema()))
+                       .build();
+       }
+
+       /**
+        * Merges two @Schema annotations, with param-level taking precedence.
+        *
+        * @param classLevel The class-level default.
+        * @param paramLevel The parameter-level override.
+        * @return Merged annotation.
+        */
+       private static Schema mergeSchemas(Schema classLevel, Schema 
paramLevel) {
+               // If parameter has a non-default schema, use it; otherwise use 
class-level
+               if (!SchemaAnnotation.empty(paramLevel))
+                       return paramLevel;
+               return classLevel;
+       }
+
        @SuppressWarnings({ "rawtypes", "unchecked" })
        @Override /* RestOpArg */
        public Object resolve(RestOpSession opSession) throws Exception {
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 63f09eda7d..075fd27eeb 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,11 +16,13 @@
  */
 package org.apache.juneau.rest.arg;
 
+import static org.apache.juneau.common.utils.StringUtils.*;
 import static org.apache.juneau.http.annotation.PathAnnotation.*;
 
 import java.lang.reflect.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
 import org.apache.juneau.collections.*;
 import org.apache.juneau.common.utils.*;
 import org.apache.juneau.http.annotation.*;
@@ -79,14 +81,98 @@ public class PathArg implements RestOpArg {
         * @param pathMatcher Path matcher for the specified method.
         */
        protected PathArg(ParamInfo paramInfo, AnnotationWorkList annotations, 
UrlPathMatcher pathMatcher) {
+               // Get the path parameter name
                this.name = getName(paramInfo, pathMatcher);
-               this.def = findDef(paramInfo).orElse(null);
+
+               // Check for class-level defaults and merge if found
+               Path mergedPath = getMergedPath(paramInfo, name);
+
+               // Use merged path annotation for all lookups
+               this.def = mergedPath != null && !mergedPath.def().isEmpty() ? 
mergedPath.def() : findDef(paramInfo).orElse(null);
                this.type = paramInfo.getParameterType().innerType();
-               this.schema = HttpPartSchema.create(Path.class, paramInfo);
+               this.schema = mergedPath != null ? 
HttpPartSchema.create(mergedPath) : HttpPartSchema.create(Path.class, 
paramInfo);
                Class<? extends HttpPartParser> pp = schema.getParser();
                this.partParser = pp != null ? 
HttpPartParser.creator().type(pp).apply(annotations).create() : null;
        }
 
+       /**
+        * Gets the merged @Path annotation combining class-level and 
parameter-level values.
+        *
+        * @param pi The parameter info.
+        * @param paramName The path parameter name.
+        * @return Merged annotation, or null if no class-level defaults exist.
+        */
+       private static Path getMergedPath(ParamInfo pi, String paramName) {
+               // Get the declaring class
+               ClassInfo declaringClass = pi.getMethod().getDeclaringClass();
+               if (declaringClass == null)
+                       return null;
+
+               // Find @Rest annotation on the class
+               Rest restAnnotation = declaringClass.getAnnotation(Rest.class);
+               if (restAnnotation == null)
+                       return null;
+
+               // Find matching @Path from class-level pathParams array
+               Path classLevelPath = null;
+               for (Path p : restAnnotation.pathParams()) {
+                       String pName = firstNonEmpty(p.name(), p.value());
+                       if (paramName.equals(pName)) {
+                               classLevelPath = p;
+                               break;
+                       }
+               }
+
+               if (classLevelPath == null)
+                       return null;
+
+               // Get parameter-level @Path
+               Path paramPath = pi.getAnnotation(Path.class);
+               if (paramPath == null)
+                       paramPath = 
pi.getParameterType().getAnnotation(Path.class);
+
+               if (paramPath == null) {
+                       // No parameter-level @Path, use class-level as-is
+                       return classLevelPath;
+               }
+
+               // Merge the two annotations: parameter-level takes precedence
+               return mergeAnnotations(classLevelPath, paramPath);
+       }
+
+       /**
+        * Merges two @Path annotations, with param-level taking precedence 
over class-level.
+        *
+        * @param classLevel The class-level default.
+        * @param paramLevel The parameter-level override.
+        * @return Merged annotation.
+        */
+       private static Path mergeAnnotations(Path classLevel, Path paramLevel) {
+               return PathAnnotation.create()
+                       .name(firstNonEmpty(paramLevel.name(), 
paramLevel.value(), classLevel.name(), classLevel.value()))
+                       .value(firstNonEmpty(paramLevel.value(), 
paramLevel.name(), classLevel.value(), classLevel.name()))
+                       .def(firstNonEmpty(paramLevel.def(), classLevel.def()))
+                       .description(paramLevel.description().length > 0 ? 
paramLevel.description() : classLevel.description())
+                       .parser(paramLevel.parser() != 
HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser())
+                       .serializer(paramLevel.serializer() != 
HttpPartSerializer.Void.class ? paramLevel.serializer() : 
classLevel.serializer())
+                       .schema(mergeSchemas(classLevel.schema(), 
paramLevel.schema()))
+                       .build();
+       }
+
+       /**
+        * Merges two @Schema annotations, with param-level taking precedence.
+        *
+        * @param classLevel The class-level default.
+        * @param paramLevel The parameter-level override.
+        * @return Merged annotation.
+        */
+       private static Schema mergeSchemas(Schema classLevel, Schema 
paramLevel) {
+               // If parameter has a non-default schema, use it; otherwise use 
class-level
+               if (!SchemaAnnotation.empty(paramLevel))
+                       return paramLevel;
+               return classLevel;
+       }
+
        private String getName(ParamInfo pi, UrlPathMatcher pathMatcher) {
                String p = findName(pi).orElse(null);
                if (p != null)
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/QueryArg.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/QueryArg.java
index a3bbd5733e..b26df97300 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/QueryArg.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/QueryArg.java
@@ -83,10 +83,16 @@ public class QueryArg implements RestOpArg {
         * @param annotations The annotations to apply to any new part parsers.
         */
        protected QueryArg(ParamInfo pi, AnnotationWorkList annotations) {
+               // Get the query name from the parameter
                this.name = findName(pi).orElseThrow(() -> new ArgException(pi, 
"@Query used without name or value"));
-               this.def = findDef(pi).orElse(null);
+
+               // Check for class-level defaults and merge if found
+               Query mergedQuery = getMergedQuery(pi, name);
+
+               // Use merged query annotation for all lookups
+               this.def = mergedQuery != null && !mergedQuery.def().isEmpty() 
? mergedQuery.def() : findDef(pi).orElse(null);
                this.type = pi.getParameterType();
-               this.schema = HttpPartSchema.create(Query.class, pi);
+               this.schema = mergedQuery != null ? 
HttpPartSchema.create(mergedQuery) : HttpPartSchema.create(Query.class, pi);
                Class<? extends HttpPartParser> pp = schema.getParser();
                this.partParser = pp != null ? 
HttpPartParser.creator().type(pp).apply(annotations).create() : null;
                this.multi = schema.getCollectionFormat() == 
HttpPartCollectionFormat.MULTI;
@@ -95,6 +101,84 @@ public class QueryArg implements RestOpArg {
                        throw new ArgException(pi, "Use of multipart flag on 
@Query parameter that is not an array or Collection");
        }
 
+       /**
+        * Gets the merged @Query annotation combining class-level and 
parameter-level values.
+        *
+        * @param pi The parameter info.
+        * @param paramName The query parameter name.
+        * @return Merged annotation, or null if no class-level defaults exist.
+        */
+       private static Query getMergedQuery(ParamInfo pi, String paramName) {
+               // Get the declaring class
+               ClassInfo declaringClass = pi.getMethod().getDeclaringClass();
+               if (declaringClass == null)
+                       return null;
+
+               // Find @Rest annotation on the class
+               Rest restAnnotation = declaringClass.getAnnotation(Rest.class);
+               if (restAnnotation == null)
+                       return null;
+
+               // Find matching @Query from class-level queryParams array
+               Query classLevelQuery = null;
+               for (Query q : restAnnotation.queryParams()) {
+                       String qName = firstNonEmpty(q.name(), q.value());
+                       if (paramName.equals(qName)) {
+                               classLevelQuery = q;
+                               break;
+                       }
+               }
+
+               if (classLevelQuery == null)
+                       return null;
+
+               // Get parameter-level @Query
+               Query paramQuery = pi.getAnnotation(Query.class);
+               if (paramQuery == null)
+                       paramQuery = 
pi.getParameterType().getAnnotation(Query.class);
+
+               if (paramQuery == null) {
+                       // No parameter-level @Query, use class-level as-is
+                       return classLevelQuery;
+               }
+
+               // Merge the two annotations: parameter-level takes precedence
+               return mergeAnnotations(classLevelQuery, paramQuery);
+       }
+
+       /**
+        * Merges two @Query annotations, with param-level taking precedence 
over class-level.
+        *
+        * @param classLevel The class-level default.
+        * @param paramLevel The parameter-level override.
+        * @return Merged annotation.
+        */
+       private static Query mergeAnnotations(Query classLevel, Query 
paramLevel) {
+               return QueryAnnotation.create()
+                       .name(firstNonEmpty(paramLevel.name(), 
paramLevel.value(), classLevel.name(), classLevel.value()))
+                       .value(firstNonEmpty(paramLevel.value(), 
paramLevel.name(), classLevel.value(), classLevel.name()))
+                       .def(firstNonEmpty(paramLevel.def(), classLevel.def()))
+                       .description(paramLevel.description().length > 0 ? 
paramLevel.description() : classLevel.description())
+                       .parser(paramLevel.parser() != 
HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser())
+                       .serializer(paramLevel.serializer() != 
HttpPartSerializer.Void.class ? paramLevel.serializer() : 
classLevel.serializer())
+                       .schema(mergeSchemas(classLevel.schema(), 
paramLevel.schema()))
+                       .build();
+       }
+
+       /**
+        * Merges two @Schema annotations, with param-level taking precedence.
+        *
+        * @param classLevel The class-level default.
+        * @param paramLevel The parameter-level override.
+        * @return Merged annotation.
+        */
+       private static Schema mergeSchemas(Schema classLevel, Schema 
paramLevel) {
+               // If parameter has a non-default schema, use it; otherwise use 
class-level
+               if (!SchemaAnnotation.empty(paramLevel))
+                       return paramLevel;
+               return classLevel;
+       }
+
        @SuppressWarnings({ "rawtypes", "unchecked" })
        @Override /* RestOpArg */
        public Object resolve(RestOpSession opSession) throws Exception {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/Query_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/Query_Test.java
index 4c1593f3ac..4a3488b644 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/Query_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/Query_Test.java
@@ -387,4 +387,97 @@ class Query_Test extends TestBase {
                        .assertStatus(200)
                        .assertContent("[{a:1,b:'foo'}]");
        }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Class-level query defaults
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(
+               queryParams={
+                       @Query(name="format", def="json", description="Output 
format"),
+                       @Query(name="verbose", def="false", 
schema=@Schema(type="boolean", description="Verbose output"))
+               }
+       )
+       public static class G {
+               @RestGet
+               public String a(@Query("format") String format, 
@Query("verbose") boolean verbose) {
+                       return "format="+format+",verbose="+verbose;
+               }
+               @RestGet
+               public String b(@Query(name="format", def="xml") String format, 
@Query("verbose") boolean verbose) {
+                       return "format="+format+",verbose="+verbose;
+               }
+       }
+
+       @Test
+       void g01_classLevelQueryDefaults() throws Exception {
+               var g = MockRestClient.build(G.class);
+
+               // Test default values from class-level @Query
+               g.get("/a").run().assertContent("format=json,verbose=false");
+
+               // Test overriding with query params
+               
g.get("/a?format=xml&verbose=true").run().assertContent("format=xml,verbose=true");
+
+               // Test method-level override of class-level default
+               g.get("/b").run().assertContent("format=xml,verbose=false");
+
+               // Test method-level override can still be overridden by query 
param
+               
g.get("/b?format=json").run().assertContent("format=json,verbose=false");
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Test class-level parameter defaults for all HTTP part types
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Rest(
+               queryParams={
+                       @Query(name="q", def="defaultQ")
+               },
+               headerParams={
+                       @Header(name="X-Custom", def="defaultHeader")
+               },
+               formDataParams={
+                       @FormData(name="f", def="defaultForm")
+               }
+       )
+       public static class H {
+               @RestGet("/query")
+               public String testQuery(@Query("q") String q) {
+                       return "q="+q;
+               }
+               @RestGet("/header")
+               public String testHeader(@Header("X-Custom") String h) {
+                       return "h="+h;
+               }
+               @RestPost("/form")
+               public String testForm(@FormData("f") String f) {
+                       return "f="+f;
+               }
+               @RestGet("/override")
+               public String testOverride(@Query(name="q", def="overrideQ") 
String q, @Header(name="X-Custom", def="overrideHeader") String h) {
+                       return "q="+q+",h="+h;
+               }
+       }
+
+       @Test
+       void h01_classLevelAllParamDefaults() throws Exception {
+               var h = MockRestClient.build(H.class);
+
+               // Test query default
+               h.get("/query").run().assertContent("q=defaultQ");
+               h.get("/query?q=custom").run().assertContent("q=custom");
+
+               // Test header default
+               h.get("/header").run().assertContent("h=defaultHeader");
+               h.get("/header").header("X-Custom", 
"custom").run().assertContent("h=custom");
+
+               // Test form data default
+               
h.post("/form").contentType("application/x-www-form-urlencoded").run().assertContent("f=defaultForm");
+               
h.post("/form").contentType("application/x-www-form-urlencoded").formData("f", 
"custom").run().assertContent("f=custom");
+
+               // Test method-level override
+               
h.get("/override").run().assertContent("q=overrideQ,h=overrideHeader");
+               h.get("/override?q=custom").header("X-Custom", 
"custom").run().assertContent("q=custom,h=custom");
+       }
 }
\ No newline at end of file


Reply via email to