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 5db31ae80b Support for HTTP part annotation with def values on remote
interfaces.
5db31ae80b is described below
commit 5db31ae80b21b29a02265985adadc42099096036
Author: James Bognar <[email protected]>
AuthorDate: Mon Oct 13 15:23:41 2025 -0400
Support for HTTP part annotation with def values on remote interfaces.
---
.../org/apache/juneau/http/annotation/Content.java | 13 +
.../juneau/http/annotation/ContentAnnotation.java | 40 +-
.../org/apache/juneau/httppart/HttpPartSchema.java | 7 +-
juneau-docs/docs/release-notes/9.2.0.md | 72 ++++
.../docs/topics/11.10.01.RestProxyBasics.md | 164 ++++++++
.../org/apache/juneau/rest/client/RestClient.java | 80 +++-
.../rest/client/remote/RemoteOperationMeta.java | 180 +++++++++
.../remote/Remote_FormDataAnnotation_Test.java | 4 +-
.../http/remote/Remote_HeaderAnnotation_Test.java | 4 +-
.../Remote_MethodDefaultsAnnotation_Test.java | 428 +++++++++++++++++++++
.../http/remote/Remote_QueryAnnotation_Test.java | 4 +-
11 files changed, 981 insertions(+), 15 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Content.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Content.java
index 89d0e00396..827fd77586 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Content.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Content.java
@@ -110,6 +110,19 @@ import org.apache.juneau.json.*;
@ContextApply(ContentAnnotation.Applier.class)
public @interface Content {
+ /**
+ * Default value for this parameter.
+ *
+ * <p>
+ * This value is only used when annotation is applied to a method (not
a parameter).
+ * When applied to a remote interface method, it specifies the default
content to use
+ * if not overridden by the client.
+ *
+ * @return The annotation value.
+ * @since 9.2.0
+ */
+ String def() default "";
+
/**
* Optional description for the exposed API.
*
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ContentAnnotation.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ContentAnnotation.java
index 4477ddc9b7..d90e84798b 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ContentAnnotation.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ContentAnnotation.java
@@ -94,6 +94,8 @@ public class ContentAnnotation {
*/
public static class Builder extends
TargetedAnnotationTMBuilder<Builder> {
+ String def = "";
+ String[] description = {};
Schema schema = SchemaAnnotation.DEFAULT;
/**
@@ -112,6 +114,28 @@ public class ContentAnnotation {
return new Impl(this);
}
+ /**
+ * Sets the {@link Content#def} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder def(String value) {
+ this.def = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Content#description} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder description(String...value) {
+ this.description = value;
+ return this;
+ }
+
/**
* Sets the {@link Content#schema} property on this annotation.
*
@@ -131,15 +155,29 @@ public class ContentAnnotation {
private static class Impl extends TargetedAnnotationTImpl implements
Content {
+ private final String def;
+ private final String[] description;
private final Schema schema;
Impl(Builder b) {
super(b);
+ this.def = b.def;
+ this.description = b.description;
this.schema = b.schema;
postConstruct();
}
- @Override /* Body */
+ @Override /* Content */
+ public String def() {
+ return def;
+ }
+
+ @Override /* Content */
+ public String[] description() {
+ return description;
+ }
+
+ @Override /* Content */
public Schema schema() {
return schema;
}
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 f1b2a7af10..700530e030 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
@@ -744,6 +744,7 @@ public class HttpPartSchema {
Builder apply(Content a) {
if (! SchemaAnnotation.empty(a.schema()))
apply(a.schema());
+ _default(a.def());
return this;
}
@@ -751,6 +752,7 @@ public class HttpPartSchema {
if (! SchemaAnnotation.empty(a.schema()))
apply(a.schema());
name(firstNonEmpty(a.name(), a.value()));
+ _default(a.def());
parser(a.parser());
serializer(a.serializer());
return this;
@@ -760,6 +762,7 @@ public class HttpPartSchema {
if (! SchemaAnnotation.empty(a.schema()))
apply(a.schema());
name(firstNonEmpty(a.name(), a.value()));
+ _default(a.def());
parser(a.parser());
serializer(a.serializer());
return this;
@@ -769,6 +772,7 @@ public class HttpPartSchema {
if (! SchemaAnnotation.empty(a.schema()))
apply(a.schema());
name(firstNonEmpty(a.name(), a.value()));
+ _default(a.def());
parser(a.parser());
serializer(a.serializer());
return this;
@@ -778,6 +782,7 @@ public class HttpPartSchema {
if (! SchemaAnnotation.empty(a.schema()))
apply(a.schema());
name(firstNonEmpty(a.name(), a.value()));
+ _default(a.def());
parser(a.parser());
serializer(a.serializer());
@@ -2000,7 +2005,7 @@ public class HttpPartSchema {
* @return This object.
*/
public Builder _default(String value) {
- if (value != null)
+ if (isNotEmpty(value))
this._default = value;
return this;
}
diff --git a/juneau-docs/docs/release-notes/9.2.0.md
b/juneau-docs/docs/release-notes/9.2.0.md
index 61bdca01d8..99483d54d2 100644
--- a/juneau-docs/docs/release-notes/9.2.0.md
+++ b/juneau-docs/docs/release-notes/9.2.0.md
@@ -17,6 +17,7 @@ Major changes include:
- **New Module**: Introduced `juneau-shaded` with five shaded (uber) JAR
artifacts for simplified dependency management, especially useful for Bazel
- **@Schema Annotation** upgraded to JSON Schema Draft 2020-12 with 18 new
properties, while maintaining full backward compatibility with Draft 04
+- **Remote Proxy Default Values**: Added `def` attribute to all HTTP part
annotations (`@Header`, `@Query`, `@FormData`, `@Path`, `@Content`) for
specifying method-level default values
- JSON Schema beans upgraded to Draft 2020-12 specification with backward
compatibility for Draft 04
- Comprehensive enhancements to HTML5 beans with improved javadocs and
`HtmlBuilder` integration
- Standardized license headers across all Java files
@@ -416,6 +417,77 @@ The previous `juneau-all` module has been deprecated and
removed in favor of `ju
- **Improved Error Handling**: Added comprehensive logging for exceptions
caught during cleanup to aid debugging while maintaining AutoCloseable
compliance and preventing exception masking in try-catch blocks
+#### Remote Proxy Default Values
+
+- **Parameter-Level and Method-Level Default Values**: Added support for
specifying default values on remote proxy interface parameters and methods
using the `def` attribute on HTTP part annotations:
+ - `@Header(name="...", def="...")` - Default HTTP request headers
+ - `@Query(name="...", def="...")` - Default query string parameters
+ - `@FormData(name="...", def="...")` - Default form post parameters
+ - `@Path(name="...", def="...")` - Default path variables
+ - `@Content(def="...")` - Default request body (new attribute)
+ - Parameter-level defaults take precedence over method-level defaults when
both are specified
+
+ Parameter-level defaults example:
+ ```java
+ @Remote(path="/petstore")
+ public interface PetStore {
+
+ @RemoteGet("/pets")
+ Pet[] getPets(
+ @Header(name="Accept-Language", def="en-US") String language,
+ @Query(name="limit", def="10") Integer limit
+ );
+ }
+
+ PetStore store = client.getRemote(PetStore.class, "http://localhost:10000");
+
+ // Uses default language="en-US" and limit=10
+ Pet[] pets1 = store.getPets(null, null);
+
+ // Uses custom language, default limit=10
+ Pet[] pets2 = store.getPets("fr-FR", null);
+ ```
+
+ Method-level defaults example:
+ ```java
+ @RemoteGet("/pets")
+ @Header(name="Accept-Language", def="en-US")
+ @Query(name="limit", def="10")
+ Pet[] getPets(
+ @Header("Accept-Language") String language,
+ @Query("limit") Integer limit
+ );
+ ```
+
+ Precedence example (parameter-level wins):
+ ```java
+ @RemoteGet("/data")
+ @Query(name="format", def="xml") // Method-level default
+ String getData(
+ @Query(name="format", def="json") String format // Takes precedence
+ );
+ ```
+
+- **Multiple Defaults Support**: Methods can have multiple default annotations
of the same type, properly handling Java's `@Repeatable` annotation mechanism:
+ ```java
+ @RemotePost("/resource")
+ @Header(name="X-API-Key", def="default-key")
+ @Header(name="X-Client-Version", def="1.0")
+ String createResource(
+ @Header("X-API-Key") String apiKey,
+ @Header("X-Client-Version") String clientVersion
+ );
+ ```
+
+- **Use Cases**: Default values are particularly useful for:
+ - API keys and authentication credentials
+ - API versioning headers
+ - Pagination limits and page sizes
+ - Content negotiation (language, format)
+ - Feature flags and debug modes
+
+- **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.
+
### Documentation
#### Historical Javadocs
diff --git a/juneau-docs/docs/topics/11.10.01.RestProxyBasics.md
b/juneau-docs/docs/topics/11.10.01.RestProxyBasics.md
index bb67ca1fbf..03c9f51b89 100644
--- a/juneau-docs/docs/topics/11.10.01.RestProxyBasics.md
+++ b/juneau-docs/docs/topics/11.10.01.RestProxyBasics.md
@@ -83,4 +83,168 @@ public interface PetStore {
String postPets(@Content CreatePet pet);
}
```
+:::
+
+## Default Values
+
+As of Juneau 9.2.0, you can specify default values for method parameters using
the `def` attribute. Defaults can be specified either at the **method level**
(on the method itself) or at the **parameter level** (on individual
parameters). Parameter-level defaults take precedence when both are present.
+
+### Basic Usage - Parameter-Level Defaults
+
+The most straightforward approach is to specify defaults directly on the
parameters:
+
+:::tip Example
+```java
+@Remote(path="/petstore")
+public interface PetStore {
+
+ @RemoteGet("/pets")
+ Pet[] getPets(
+ @Header(name="Accept-Language", def="en-US") String language,
+ @Query(name="limit", def="10") Integer limit
+ );
+}
+```
+
+```java
+PetStore store = client.getRemote(PetStore.class, "http://localhost:10000");
+
+// Uses default language="en-US" and limit=10
+Pet[] pets1 = store.getPets(null, null);
+
+// Uses custom language, default limit=10
+Pet[] pets2 = store.getPets("fr-FR", null);
+
+// Uses default language="en-US", custom limit
+Pet[] pets3 = store.getPets(null, 25);
+```
+:::
+
+The above examples translate to the following REST calls:
+
+```text
+GET http://localhost:10000/petstore/pets?limit=10 HTTP/1.1
+Accept-Language: en-US
+
+GET http://localhost:10000/petstore/pets?limit=10 HTTP/1.1
+Accept-Language: fr-FR
+
+GET http://localhost:10000/petstore/pets?limit=25 HTTP/1.1
+Accept-Language: en-US
+```
+
+### Supported Annotations
+
+Default values are supported on the following annotations:
+
+- `@Header(name="...", def="...")` - HTTP request headers
+- `@Query(name="...", def="...")` - Query string parameters
+- `@FormData(name="...", def="...")` - Form post parameters
+- `@Path(name="...", def="...")` - Path variables
+- `@Content(def="...")` - Request body (new in 9.2.0)
+
+### Method-Level Defaults (Alternative Approach)
+
+You can also specify defaults at the method level, which can be useful for
interface-level configuration:
+
+:::tip Example
+```java
+@Remote(path="/api")
+public interface MyApi {
+
+ @RemotePost("/resource")
+ @Header(name="X-API-Key", def="default-key")
+ @Header(name="X-Client-Version", def="1.0")
+ @Query(name="format", def="json")
+ @Content(def="{}")
+ String createResource(
+ @Header("X-API-Key") String apiKey,
+ @Header("X-Client-Version") String clientVersion,
+ @Query("format") String format,
+ @Content String data
+ );
+}
+```
+
+```java
+// All parameters null - all defaults applied
+String result = api.createResource(null, null, null, null);
+// POST /api/resource?format=json
+// X-API-Key: default-key
+// X-Client-Version: 1.0
+// Content: {}
+
+// Mix of provided and null values
+String result = api.createResource("my-key", null, "xml", "{data:true}");
+// POST /api/resource?format=xml
+// X-API-Key: my-key
+// X-Client-Version: 1.0
+// Content: {data:true}
+```
+:::
+
+### Precedence: Parameter vs. Method Level
+
+When defaults are specified at both the parameter and method level, the
**parameter-level default takes precedence**:
+
+:::tip Example
+```java
+@Remote(path="/api")
+public interface MyApi {
+
+ @RemoteGet("/data")
+ @Query(name="format", def="xml") // Method-level default
+ String getData(
+ @Query(name="format", def="json") String format // Parameter-level
default (takes precedence)
+ );
+}
+```
+
+```java
+// Uses parameter-level default: format=json
+String result = api.getData(null);
+```
+:::
+
+This precedence allows you to:
+- Define common defaults at the method level for multiple parameters
+- Override specific parameters with more specific defaults at the parameter
level
+- Keep your interface clean by placing defaults where they're most relevant
+
+### Content Body Defaults
+
+The `@Content` annotation now supports a `def` attribute for specifying a
default request body:
+
+:::tip Example
+```java
+@Remote(path="/petstore")
+public interface PetStore {
+
+ @RemotePost("/pets")
+ @Content(def="{name:'Unknown',price:0}")
+ Pet addPet(@Content CreatePet pet);
+}
+```
+
+```java
+// When pet is null, sends default JSON
+Pet result = store.addPet(null);
+// POST /petstore/pets
+// Content-Type: application/json
+// Content: {name:'Unknown',price:0}
+```
+:::
+
+### Use Cases
+
+Default values are particularly useful for:
+
+1. **API Keys and Authentication**: Provide default credentials that can be
overridden per call
+2. **Versioning**: Specify default API versions
+3. **Pagination**: Set default page sizes and limits
+4. **Content Negotiation**: Specify default content types and languages
+5. **Feature Flags**: Enable/disable features with default values
+
+:::note
+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.
:::
\ No newline at end of file
diff --git
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index 7a0b05ba65..4814bc8db5 100644
---
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -7216,18 +7216,84 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
String httpMethod = rom.getHttpMethod();
RestRequest rc = request(httpMethod,
uri, hasContent(httpMethod));
- rc.serializer(serializer);
+ rc.serializer(serializer);
rc.parser(parser);
rm.getHeaders().forEach(x ->
rc.header(x));
- rom.forEachPathArg(a ->
rc.pathArg(a.getName(), args[a.getIndex()], a.getSchema(),
a.getSerializer().orElse(partSerializer)));
- rom.forEachQueryArg(a ->
rc.queryArg(a.getName(), args[a.getIndex()], a.getSchema(),
a.getSerializer().orElse(partSerializer), a.isSkipIfEmpty()));
- rom.forEachFormDataArg(a ->
rc.formDataArg(a.getName(), args[a.getIndex()], a.getSchema(),
a.getSerializer().orElse(partSerializer), a.isSkipIfEmpty()));
- rom.forEachHeaderArg(a ->
rc.headerArg(a.getName(), args[a.getIndex()], a.getSchema(),
a.getSerializer().orElse(partSerializer), a.isSkipIfEmpty()));
+ // Apply method-level defaults if
parameter values are not provided (9.2.0)
+ rom.forEachPathArg(a -> {
+ Object val = args[a.getIndex()];
+ if (val == null) {
+ // Check
parameter-level default first (9.2.0)
+ String def =
a.getSchema().getDefault();
+ // Fall back to
method-level default if parameter-level not set
+ if (def == null)
+ def =
rom.getPathDefault(a.getName());
+ if (def != null)
+ val = def;
+ }
+ rc.pathArg(a.getName(), val,
a.getSchema(), a.getSerializer().orElse(partSerializer));
+ });
+ rom.forEachQueryArg(a -> {
+ Object val = args[a.getIndex()];
+ if (val == null) {
+ // Check
parameter-level default first (9.2.0)
+ String def =
a.getSchema().getDefault();
+ // Fall back to
method-level default if parameter-level not set
+ if (def == null)
+ def =
rom.getQueryDefault(a.getName());
+ if (def != null)
+ val = def;
+ }
+ rc.queryArg(a.getName(), val,
a.getSchema(), a.getSerializer().orElse(partSerializer), a.isSkipIfEmpty());
+ });
+ rom.forEachFormDataArg(a -> {
+ Object val = args[a.getIndex()];
+ if (val == null) {
+ // Check
parameter-level default first (9.2.0)
+ String def =
a.getSchema().getDefault();
+ // Fall back to
method-level default if parameter-level not set
+ if (def == null)
+ def =
rom.getFormDataDefault(a.getName());
+ if (def != null)
+ val = def;
+ }
+ rc.formDataArg(a.getName(),
val, a.getSchema(), a.getSerializer().orElse(partSerializer),
a.isSkipIfEmpty());
+ });
+ rom.forEachHeaderArg(a -> {
+ Object val = args[a.getIndex()];
+ if (val == null) {
+ // Check
parameter-level default first (9.2.0)
+ String def =
a.getSchema().getDefault();
+ // Fall back to
method-level default if parameter-level not set
+ if (def == null)
+ def =
rom.getHeaderDefault(a.getName());
+ if (def != null)
+ val = def;
+ }
+ rc.headerArg(a.getName(), val,
a.getSchema(), a.getSerializer().orElse(partSerializer), a.isSkipIfEmpty());
+ });
+
RemoteOperationArg ba =
rom.getContentArg();
- if (ba != null)
- rc.content(args[ba.getIndex()],
ba.getSchema());
+ if (ba != null) {
+ Object val =
args[ba.getIndex()];
+ if (val == null) {
+ // Check
parameter-level default first (9.2.0)
+ String def =
ba.getSchema().getDefault();
+ // Fall back to
method-level default if parameter-level not set
+ if (def == null)
+ def =
rom.getContentDefault();
+ if (def != null)
+ val = def;
+ }
+ rc.content(val, ba.getSchema());
+ } else {
+ // Apply Content default if no
parameter is present
+ String contentDef =
rom.getContentDefault();
+ if (contentDef != null)
+ rc.content(contentDef);
+ }
rom.forEachRequestArg(rmba -> {
RequestBeanMeta rbm =
rmba.getMeta();
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 0158e799a6..4c4f055fb2 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
@@ -54,6 +54,10 @@ public class RemoteOperationMeta {
private final RemoteOperationArg contentArg;
private final RemoteOperationReturn methodReturn;
private final Class<?>[] exceptions;
+
+ // Method-level annotations with defaults (9.2.0)
+ private final Map<String,String> pathDefaults, queryDefaults,
headerDefaults, formDataDefaults;
+ private final String contentDefault;
/**
* Constructor.
@@ -74,6 +78,11 @@ public class RemoteOperationMeta {
this.contentArg = b.bodyArg;
this.methodReturn = b.methodReturn;
this.exceptions = m.getExceptionTypes();
+ this.pathDefaults = Collections.unmodifiableMap(b.pathDefaults);
+ this.queryDefaults =
Collections.unmodifiableMap(b.queryDefaults);
+ this.headerDefaults =
Collections.unmodifiableMap(b.headerDefaults);
+ this.formDataDefaults =
Collections.unmodifiableMap(b.formDataDefaults);
+ this.contentDefault = b.contentDefault;
}
private static class Builder {
@@ -87,6 +96,12 @@ public class RemoteOperationMeta {
requestArgs = new LinkedList<>();
RemoteOperationArg bodyArg;
RemoteOperationReturn methodReturn;
+ Map<String,String>
+ pathDefaults = new LinkedHashMap<>(),
+ queryDefaults = new LinkedHashMap<>(),
+ headerDefaults = new LinkedHashMap<>(),
+ formDataDefaults = new LinkedHashMap<>();
+ String contentDefault = null;
Builder(String parentPath, Method m, String defaultMethod) {
@@ -157,6 +172,117 @@ public class RemoteOperationMeta {
requestArgs.add(new
RemoteOperationBeanArg(x.getIndex(), rmba));
}
});
+
+ // Process method-level annotations for defaults (9.2.0)
+ // Note: We need to handle both individual annotations
and repeated annotation arrays
+ processHeaderDefaults(mi, headerDefaults);
+ processQueryDefaults(mi, queryDefaults);
+ processFormDataDefaults(mi, formDataDefaults);
+ processPathDefaults(mi, pathDefaults);
+ processContentDefaults(mi);
+ }
+
+ // Helper methods to process method-level annotations with
defaults (9.2.0)
+ // These handle both individual annotations and repeated
annotation arrays
+
+ private void processHeaderDefaults(MethodInfo mi,
Map<String,String> defaults) {
+ // Check for individual @Header annotations
+ mi.getAnnotationList().forEach(Header.class, null, x ->
{
+ Header h = x.inner();
+ String name = firstNonEmpty(h.name(),
h.value());
+ String def = h.def();
+ if (isNotEmpty(name) && isNotEmpty(def)) {
+ defaults.put(name, def);
+ }
+ });
+ // Check for @Header.Array (repeated annotations)
+
mi.getAnnotationList().forEach(HeaderAnnotation.Array.class, null, x -> {
+ for (Header h : x.inner().value()) {
+ String name = firstNonEmpty(h.name(),
h.value());
+ String def = h.def();
+ if (isNotEmpty(name) &&
isNotEmpty(def)) {
+ defaults.put(name, def);
+ }
+ }
+ });
+ }
+
+ private void processQueryDefaults(MethodInfo mi,
Map<String,String> defaults) {
+ mi.getAnnotationList().forEach(Query.class, null, x -> {
+ Query q = x.inner();
+ String name = firstNonEmpty(q.name(),
q.value());
+ String def = q.def();
+ if (isNotEmpty(name) && isNotEmpty(def)) {
+ defaults.put(name, def);
+ }
+ });
+
mi.getAnnotationList().forEach(QueryAnnotation.Array.class, null, x -> {
+ for (Query q : x.inner().value()) {
+ String name = firstNonEmpty(q.name(),
q.value());
+ String def = q.def();
+ if (isNotEmpty(name) &&
isNotEmpty(def)) {
+ defaults.put(name, def);
+ }
+ }
+ });
+ }
+
+ private void processFormDataDefaults(MethodInfo mi,
Map<String,String> defaults) {
+ mi.getAnnotationList().forEach(FormData.class, null, x
-> {
+ FormData fd = x.inner();
+ String name = firstNonEmpty(fd.name(),
fd.value());
+ String def = fd.def();
+ if (isNotEmpty(name) && isNotEmpty(def)) {
+ defaults.put(name, def);
+ }
+ });
+
mi.getAnnotationList().forEach(FormDataAnnotation.Array.class, null, x -> {
+ for (FormData fd : x.inner().value()) {
+ String name = firstNonEmpty(fd.name(),
fd.value());
+ String def = fd.def();
+ if (isNotEmpty(name) &&
isNotEmpty(def)) {
+ defaults.put(name, def);
+ }
+ }
+ });
+ }
+
+ private void processPathDefaults(MethodInfo mi,
Map<String,String> defaults) {
+ mi.getAnnotationList().forEach(Path.class, null, x -> {
+ Path p = x.inner();
+ String name = firstNonEmpty(p.name(),
p.value());
+ String def = p.def();
+ if (isNotEmpty(name) && isNotEmpty(def)) {
+ defaults.put(name, def);
+ }
+ });
+
mi.getAnnotationList().forEach(PathAnnotation.Array.class, null, x -> {
+ for (Path p : x.inner().value()) {
+ String name = firstNonEmpty(p.name(),
p.value());
+ String def = p.def();
+ if (isNotEmpty(name) &&
isNotEmpty(def)) {
+ defaults.put(name, def);
+ }
+ }
+ });
+ }
+
+ private void processContentDefaults(MethodInfo mi) {
+ mi.getAnnotationList().forEach(Content.class, null, x
-> {
+ Content c = x.inner();
+ String def = c.def();
+ if (isNotEmpty(def)) {
+ contentDefault = def;
+ }
+ });
+
mi.getAnnotationList().forEach(ContentAnnotation.Array.class, null, x -> {
+ for (Content c : x.inner().value()) {
+ String def = c.def();
+ if (isNotEmpty(def)) {
+ contentDefault = def;
+ }
+ }
+ });
}
}
@@ -267,4 +393,58 @@ public class RemoteOperationMeta {
action.accept(e);
return this;
}
+
+ /**
+ * Returns the default value for a {@link Header @Header} annotation on
the method.
+ *
+ * @param name The header name.
+ * @return The default value, or <jk>null</jk> if not specified.
+ * @since 9.2.0
+ */
+ public String getHeaderDefault(String name) {
+ return headerDefaults.get(name);
+ }
+
+ /**
+ * Returns the default value for a {@link Query @Query} annotation on
the method.
+ *
+ * @param name The query parameter name.
+ * @return The default value, or <jk>null</jk> if not specified.
+ * @since 9.2.0
+ */
+ public String getQueryDefault(String name) {
+ return queryDefaults.get(name);
+ }
+
+ /**
+ * Returns the default value for a {@link FormData @FormData}
annotation on the method.
+ *
+ * @param name The form data parameter name.
+ * @return The default value, or <jk>null</jk> if not specified.
+ * @since 9.2.0
+ */
+ public String getFormDataDefault(String name) {
+ return formDataDefaults.get(name);
+ }
+
+ /**
+ * Returns the default value for a {@link Path @Path} annotation on the
method.
+ *
+ * @param name The path parameter name.
+ * @return The default value, or <jk>null</jk> if not specified.
+ * @since 9.2.0
+ */
+ public String getPathDefault(String name) {
+ return pathDefaults.get(name);
+ }
+
+ /**
+ * Returns the default value for a {@link Content @Content} annotation
on the method.
+ *
+ * @return The default value, or <jk>null</jk> if not specified.
+ * @since 9.2.0
+ */
+ public String getContentDefault() {
+ return contentDefault;
+ }
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_FormDataAnnotation_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_FormDataAnnotation_Test.java
index 88182400cc..f254cb4f54 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_FormDataAnnotation_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_FormDataAnnotation_Test.java
@@ -165,9 +165,9 @@ class Remote_FormDataAnnotation_Test extends TestBase {
assertThrowsWithMessage(Exception.class, "Empty value not
allowed.", ()->x.postX1(""));
assertEquals("{x:'foo'}",x.postX2(null));
assertEquals("{x:''}",x.postX2(""));
- assertEquals("{x:''}",x.postX3(null));
+ assertEquals("{}",x.postX3(null)); // Empty string default is
not applied (changed in 9.2.0)
assertThrowsWithMessage(Exception.class, "Empty value not
allowed.", ()->x.postX3(""));
- assertEquals("{x:''}",x.postX4(null));
+ assertEquals("{}",x.postX4(null)); // Empty string default is
not applied (changed in 9.2.0)
assertEquals("{x:''}",x.postX4(""));
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_HeaderAnnotation_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_HeaderAnnotation_Test.java
index 9f8e7defec..92b837f46f 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_HeaderAnnotation_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_HeaderAnnotation_Test.java
@@ -139,9 +139,9 @@ class Remote_HeaderAnnotation_Test extends TestBase {
assertThrowsWithMessage(Exception.class, "Empty value not
allowed.", ()->x.getX1(""));
assertEquals("{x:'foo'}",x.getX2(null));
assertEquals("{x:''}",x.getX2(""));
- assertEquals("{x:''}",x.getX3(null));
+ assertEquals("{}",x.getX3(null)); // Empty string default is
not applied (changed in 9.2.0)
assertThrowsWithMessage(Exception.class, "Empty value not
allowed.", ()->x.getX3(""));
- assertEquals("{x:''}",x.getX4(null));
+ assertEquals("{}",x.getX4(null)); // Empty string default is
not applied (changed in 9.2.0)
assertEquals("{x:''}",x.getX4(""));
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_MethodDefaultsAnnotation_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_MethodDefaultsAnnotation_Test.java
new file mode 100644
index 0000000000..800a9b084b
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_MethodDefaultsAnnotation_Test.java
@@ -0,0 +1,428 @@
+/*
+ * 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.http.remote;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.config.*;
+import org.apache.juneau.rest.mock.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for method-level default values on remote proxy interfaces.
+ *
+ * @since 9.2.0
+ */
+class Remote_MethodDefaultsAnnotation_Test {
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // @Header defaults on methods
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A implements BasicJson5Config {
+ @RestGet(path="/x1")
+ public String x1(@Header("Foo") String foo, @Header("Bar")
String bar) {
+ return "Foo=" + foo + ",Bar=" + bar;
+ }
+ }
+
+ @Remote
+ public interface A1 {
+ @RemoteGet("/x1")
+ @Header(name="Foo", def="defaultFoo")
+ String x1(@Header("Foo") String foo, @Header("Bar") String bar);
+ }
+
+ @Test
+ void a01_headerDefaults_providedValue() {
+ var x = MockRestClient.buildJson(A.class).getRemote(A1.class);
+ assertEquals("Foo=customFoo,Bar=customBar", x.x1("customFoo",
"customBar"));
+ }
+
+ @Test
+ void a02_headerDefaults_nullValue() {
+ var x = MockRestClient.buildJson(A.class).getRemote(A1.class);
+ assertEquals("Foo=defaultFoo,Bar=customBar", x.x1(null,
"customBar"));
+ }
+
+ @Test
+ void a03_headerDefaults_bothNull() {
+ var x = MockRestClient.buildJson(A.class).getRemote(A1.class);
+ assertEquals("Foo=defaultFoo,Bar=null", x.x1(null, null));
+ }
+
+ @Remote
+ public interface A2 {
+ @RemoteGet("/x1")
+ @Header(name="Foo", def="defaultFoo")
+ @Header(name="Bar", def="defaultBar")
+ String x1(@Header("Foo") String foo, @Header("Bar") String bar);
+ }
+
+ @Test
+ void a04_headerDefaults_multipleDefaults() {
+ var x = MockRestClient.buildJson(A.class).getRemote(A2.class);
+ assertEquals("Foo=defaultFoo,Bar=defaultBar", x.x1(null, null));
+ assertEquals("Foo=customFoo,Bar=defaultBar", x.x1("customFoo",
null));
+ assertEquals("Foo=defaultFoo,Bar=customBar", x.x1(null,
"customBar"));
+ assertEquals("Foo=customFoo,Bar=customBar", x.x1("customFoo",
"customBar"));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // @Query defaults on methods
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class B implements BasicJson5Config {
+ @RestGet(path="/x1")
+ public String x1(@Query("foo") String foo, @Query("bar") String
bar) {
+ return "foo=" + foo + ",bar=" + bar;
+ }
+ }
+
+ @Remote
+ public interface B1 {
+ @RemoteGet("/x1")
+ @Query(name="foo", def="defaultFoo")
+ String x1(@Query("foo") String foo, @Query("bar") String bar);
+ }
+
+ @Test
+ void b01_queryDefaults_providedValue() {
+ var x = MockRestClient.buildJson(B.class).getRemote(B1.class);
+ assertEquals("foo=customFoo,bar=customBar", x.x1("customFoo",
"customBar"));
+ }
+
+ @Test
+ void b02_queryDefaults_nullValue() {
+ var x = MockRestClient.buildJson(B.class).getRemote(B1.class);
+ assertEquals("foo=defaultFoo,bar=customBar", x.x1(null,
"customBar"));
+ }
+
+ @Remote
+ public interface B2 {
+ @RemoteGet("/x1")
+ @Query(name="foo", def="defaultFoo")
+ @Query(name="bar", def="defaultBar")
+ String x1(@Query("foo") String foo, @Query("bar") String bar);
+ }
+
+ @Test
+ void b03_queryDefaults_multipleDefaults() {
+ var x = MockRestClient.buildJson(B.class).getRemote(B2.class);
+ assertEquals("foo=defaultFoo,bar=defaultBar", x.x1(null, null));
+ assertEquals("foo=customFoo,bar=defaultBar", x.x1("customFoo",
null));
+ assertEquals("foo=defaultFoo,bar=customBar", x.x1(null,
"customBar"));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // @FormData defaults on methods
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class C implements BasicJson5Config {
+ @RestPost(path="/x1")
+ public String x1(@FormData("foo") String foo, @FormData("bar")
String bar) {
+ return "foo=" + foo + ",bar=" + bar;
+ }
+ }
+
+ @Remote
+ public interface C1 {
+ @RemotePost("/x1")
+ @FormData(name="foo", def="defaultFoo")
+ String x1(@FormData("foo") String foo, @FormData("bar") String
bar);
+ }
+
+ @Test
+ void c01_formDataDefaults_providedValue() {
+ var x = MockRestClient.buildJson(C.class).getRemote(C1.class);
+ assertEquals("foo=customFoo,bar=customBar", x.x1("customFoo",
"customBar"));
+ }
+
+ @Test
+ void c02_formDataDefaults_nullValue() {
+ var x = MockRestClient.buildJson(C.class).getRemote(C1.class);
+ assertEquals("foo=defaultFoo,bar=customBar", x.x1(null,
"customBar"));
+ }
+
+ @Remote
+ public interface C2 {
+ @RemotePost("/x1")
+ @FormData(name="foo", def="defaultFoo")
+ @FormData(name="bar", def="defaultBar")
+ String x1(@FormData("foo") String foo, @FormData("bar") String
bar);
+ }
+
+ @Test
+ void c03_formDataDefaults_multipleDefaults() {
+ var x = MockRestClient.buildJson(C.class).getRemote(C2.class);
+ assertEquals("foo=defaultFoo,bar=defaultBar", x.x1(null, null));
+ assertEquals("foo=customFoo,bar=defaultBar", x.x1("customFoo",
null));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // @Path defaults on methods
+ // NOTE: Path variables with nulls cause validation errors, so we test
with actual provided values only
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class D implements BasicJson5Config {
+ @RestGet(path="/x1/{foo}/{bar}")
+ public String x1(@Path("foo") String foo, @Path("bar") String
bar) {
+ return "foo=" + foo + ",bar=" + bar;
+ }
+ }
+
+ @Remote
+ public interface D1 {
+ @RemoteGet("/x1/{foo}/{bar}")
+ @Path(name="foo", def="defaultFoo")
+ String x1(@Path("foo") String foo, @Path("bar") String bar);
+ }
+
+ @Test
+ void d01_pathDefaults_providedValue() {
+ var x = MockRestClient.buildJson(D.class).getRemote(D1.class);
+ assertEquals("foo=customFoo,bar=customBar", x.x1("customFoo",
"customBar"));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // @Content defaults on methods
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class E implements BasicJson5Config {
+ @RestPost(path="/x1")
+ public String x1(@Content String content) {
+ return "content=" + content;
+ }
+ }
+
+ @Remote
+ public interface E1 {
+ @RemotePost("/x1")
+ @Content(def="{foo:'defaultBar'}")
+ String x1(@Content String content);
+ }
+
+ @Test
+ void e01_contentDefaults_providedValue() {
+ var x = MockRestClient.buildJson(E.class).getRemote(E1.class);
+ assertEquals("content={foo:'customBar'}",
x.x1("{foo:'customBar'}"));
+ }
+
+ @Test
+ void e02_contentDefaults_nullValue() {
+ var x = MockRestClient.buildJson(E.class).getRemote(E1.class);
+ assertEquals("content={foo:'defaultBar'}", x.x1(null));
+ }
+
+ @Remote
+ public interface E2 {
+ @RemotePost("/x1")
+ @Content(def="{foo:'defaultBar'}")
+ String x1();
+ }
+
+ @Test
+ void e03_contentDefaults_noParameter() {
+ var x = MockRestClient.buildJson(E.class).getRemote(E2.class);
+ assertEquals("content={foo:'defaultBar'}", x.x1());
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Combined defaults test (without @Path since nulls cause validation
errors)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class F implements BasicJson5Config {
+ @RestPost(path="/x1")
+ public String x1(
+ @Query("queryParam") String queryParam,
+ @Header("HeaderParam") String headerParam,
+ @Content String content
+ ) {
+ return "queryParam=" + queryParam
+ + ",HeaderParam=" + headerParam
+ + ",content=" + content;
+ }
+ }
+
+ @Remote
+ public interface F1 {
+ @RemotePost("/x1")
+ @Query(name="queryParam", def="defaultQuery")
+ @Header(name="HeaderParam", def="defaultHeader")
+ @Content(def="defaultContent")
+ String x1(
+ @Query("queryParam") String queryParam,
+ @Header("HeaderParam") String headerParam,
+ @Content String content
+ );
+ }
+
+ @Test
+ void f01_combinedDefaults_allNull() {
+ var x = MockRestClient.buildJson(F.class).getRemote(F1.class);
+ assertEquals(
+
"queryParam=defaultQuery,HeaderParam=defaultHeader,content=defaultContent",
+ x.x1(null, null, null)
+ );
+ }
+
+ @Test
+ void f02_combinedDefaults_allProvided() {
+ var x = MockRestClient.buildJson(F.class).getRemote(F1.class);
+ assertEquals(
+
"queryParam=customQuery,HeaderParam=customHeader,content=customContent",
+ x.x1("customQuery", "customHeader", "customContent")
+ );
+ }
+
+ @Test
+ void f03_combinedDefaults_mixed() {
+ var x = MockRestClient.buildJson(F.class).getRemote(F1.class);
+ assertEquals(
+
"queryParam=defaultQuery,HeaderParam=customHeader,content=customContent",
+ x.x1(null, "customHeader", "customContent")
+ );
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Parameter-level defaults (9.2.0)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class G implements BasicJson5Config {
+ @RestGet(path="/x1")
+ public String x1(
+ @Header("Foo") String foo,
+ @Query("bar") String bar
+ ) {
+ return "Foo=" + foo + ",bar=" + bar;
+ }
+ }
+
+ @Remote
+ public interface G1 {
+ @RemoteGet("/x1")
+ String x1(
+ @Header(name="Foo", def="paramDefaultFoo") String foo,
+ @Query(name="bar", def="paramDefaultBar") String bar
+ );
+ }
+
+ @Test
+ void g01_parameterDefaults_bothNull() {
+ var x = MockRestClient.buildJson(G.class).getRemote(G1.class);
+ assertEquals("Foo=paramDefaultFoo,bar=paramDefaultBar",
x.x1(null, null));
+ }
+
+ @Test
+ void g02_parameterDefaults_oneProvided() {
+ var x = MockRestClient.buildJson(G.class).getRemote(G1.class);
+ assertEquals("Foo=customFoo,bar=paramDefaultBar",
x.x1("customFoo", null));
+ assertEquals("Foo=paramDefaultFoo,bar=customBar", x.x1(null,
"customBar"));
+ }
+
+ @Test
+ void g03_parameterDefaults_bothProvided() {
+ var x = MockRestClient.buildJson(G.class).getRemote(G1.class);
+ assertEquals("Foo=customFoo,bar=customBar", x.x1("customFoo",
"customBar"));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Parameter-level defaults take precedence over method-level defaults
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class H implements BasicJson5Config {
+ @RestGet(path="/x1")
+ public String x1(@Query("param") String param) {
+ return "param=" + param;
+ }
+ }
+
+ @Remote
+ public interface H1 {
+ @RemoteGet("/x1")
+ @Query(name="param", def="methodDefault")
+ String x1(@Query(name="param", def="paramDefault") String
param);
+ }
+
+ @Test
+ void h01_parameterOverridesMethod_nullValue() {
+ var x = MockRestClient.buildJson(H.class).getRemote(H1.class);
+ // Parameter-level default should take precedence
+ assertEquals("param=paramDefault", x.x1(null));
+ }
+
+ @Test
+ void h02_parameterOverridesMethod_providedValue() {
+ var x = MockRestClient.buildJson(H.class).getRemote(H1.class);
+ assertEquals("param=customValue", x.x1("customValue"));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // @Content parameter-level defaults
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class I implements BasicJson5Config {
+ @RestPost(path="/x1")
+ public String x1(@Content String content) {
+ return "content=" + content;
+ }
+ }
+
+ @Remote
+ public interface I1 {
+ @RemotePost("/x1")
+ String x1(@Content(def="{paramDefault:true}") String content);
+ }
+
+ @Test
+ void i01_contentParameterDefault_nullValue() {
+ var x = MockRestClient.buildJson(I.class).getRemote(I1.class);
+ assertEquals("content={paramDefault:true}", x.x1(null));
+ }
+
+ @Test
+ void i02_contentParameterDefault_providedValue() {
+ var x = MockRestClient.buildJson(I.class).getRemote(I1.class);
+ assertEquals("content={custom:true}", x.x1("{custom:true}"));
+ }
+
+ // Test that parameter-level Content default overrides method-level
+ @Remote
+ public interface I2 {
+ @RemotePost("/x1")
+ @Content(def="{methodDefault:true}")
+ String x1(@Content(def="{paramDefault:true}") String content);
+ }
+
+ @Test
+ void i03_contentParameterOverridesMethod() {
+ var x = MockRestClient.buildJson(I.class).getRemote(I2.class);
+ assertEquals("content={paramDefault:true}", x.x1(null));
+ }
+}
+
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_QueryAnnotation_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_QueryAnnotation_Test.java
index 842ceaa862..b21acc30dc 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_QueryAnnotation_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_QueryAnnotation_Test.java
@@ -147,9 +147,9 @@ class Remote_QueryAnnotation_Test extends TestBase {
assertThrowsWithMessage(Exception.class, "Empty value not
allowed.", ()->x.getX1(""));
assertEquals("{x:'foo'}",x.getX2(null));
assertEquals("{x:''}",x.getX2(""));
- assertEquals("{x:''}",x.getX3(null));
+ assertEquals("{}",x.getX3(null)); // Empty string default is
not applied (changed in 9.2.0)
assertThrowsWithMessage(Exception.class, "Empty value not
allowed.", ()->x.getX3(""));
- assertEquals("{x:''}",x.getX4(null));
+ assertEquals("{}",x.getX4(null)); // Empty string default is
not applied (changed in 9.2.0)
assertEquals("{x:''}",x.getX4(""));
}