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