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 d560956ed9 Add @PathRemainder annotation
d560956ed9 is described below
commit d560956ed9d496f26d88b8e9559ec32b0f91f6b7
Author: James Bognar <[email protected]>
AuthorDate: Wed Oct 15 09:28:23 2025 -0400
Add @PathRemainder annotation
---
TODO.txt | 33 +--
.../juneau/http/annotation/PathRemainder.java | 179 +++++++++++++
.../http/annotation/PathRemainderAnnotation.java | 288 +++++++++++++++++++++
.../org/apache/juneau/httppart/HttpPartSchema.java | 18 ++
juneau-docs/docs/release-notes/9.2.0.md | 41 +++
juneau-docs/docs/topics/09.03.04.PathVariables.md | 30 +++
juneau-docs/docs/topics/11.10.08.Path.md | 21 ++
.../rest/client/remote/RemoteOperationArg.java | 13 +
.../java/org/apache/juneau/rest/RestContext.java | 1 +
.../apache/juneau/rest/arg/PathRemainderArg.java | 102 ++++++++
.../juneau/rest/annotation/PathRemainder_Test.java | 154 +++++++++++
scripts/README-check-fluent-setter-overrides.md | 164 ++++++++++++
scripts/check-fluent-setter-overrides.py | 273 +++++++++++++++++++
13 files changed, 1286 insertions(+), 31 deletions(-)
diff --git a/TODO.txt b/TODO.txt
index bcff129e03..b45961b3d6 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,41 +1,12 @@
-ClassInfo improvements to getMethod (e.g. getMethodExact vs getMethod).Re-add
@PathRemainder annotation.
+ClassInfo improvements to getMethod (e.g. getMethodExact vs getMethod).
+Re-add @PathRemainder annotation.
Thrown NotFound causes - javax.servlet.ServletException: Invalid method
response: 200
-Replace @Bean(findFluentSetters) with @FluentSetters.
HttpResponse should use list of Headers and have a headers(Header...) method.
HttpResponse should allow you to set code.
HttpException subclasses can set status, but does it use code?
HttpException should use list of Headers and have a headers(Header...) method.
-JsonSchema should have fluent getters and setters.
@ResponseBody and @ResponseHeaders shouldn't be required on HttpResponse
objects.
-This has to be easier:
- @Enumerated(STRING)
- @Schema(description="Routing types that this directive applies to.")
- @NotEmpty(message="At least one copy type is required")
- @Fetched(primary=true, fetcher=CopyTypes.class)
- @SortNatural
- @Beanp(type=TreeSet.class, params=CopyType.class)
- protected Set<CopyType> copyTypes;
-
- public Set<CopyType> getCopyTypes() {
- return copyTypes;
- }
-
- public Directive setCopyTypes(Set<CopyType> value) {
- this.copyTypes = value;
- return this;
- }
-
-assertBodyMatches should tell you at which position it differs and make it
obvious in the error.
-
-
- .extracting(Directive::getLabel, Directive::getStatus,
Directive::getStart, Directive::getEnd)
- .containsOnly(d.getLabel(), DirectiveStatus.ACTIVE, d.getStart(),
d.getEnd());
-
- assertThat(releaseService.sortReleases(Arrays.asList(sb0_224, sb0_226,
sb0_222))).containsExactly(sb0_222, sb0_224, sb0_226);
-
-
-Better support for SortedSet properties.
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainder.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainder.java
new file mode 100644
index 0000000000..e89a840a0e
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainder.java
@@ -0,0 +1,179 @@
+/*
+ * 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.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.oapi.*;
+
+/**
+ * REST request path remainder annotation.
+ *
+ * <p>
+ * Identifies a POJO to be used as the path remainder (the part after the path
match) on an HTTP request.
+ *
+ * <p>
+ * This is a specialized shortcut for <c><ja>@Path</ja>(<js>"/*"</js>)</c>.
+ *
+ * <p>
+ * Can be used in the following locations:
+ * <ul>
+ * <li>Arguments and argument-types of server-side
<ja>@RestOp</ja>-annotated methods.
+ * <li>Arguments and argument-types of client-side
<ja>@RemoteResource</ja>-annotated interfaces.
+ * <li>Methods and return types of server-side and client-side
<ja>@Request</ja>-annotated interfaces.
+ * </ul>
+ *
+ * <h5 class='topic'>Arguments and argument-types of server-side
@RestOp-annotated methods</h5>
+ * <p>
+ * Annotation that can be applied to a parameter of a
<ja>@RestOp</ja>-annotated method to identify it as the
+ * path remainder after the path pattern match.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <ja>@RestGet</ja>(<js>"/myurl/{foo}/{bar}/*"</js>)
+ * <jk>public void</jk> doGet(
+ * <ja>@Path</ja>(<js>"foo"</js>) String <jv>foo</jv>,
+ * <ja>@Path</ja>(<js>"bar"</js>) <jk>int</jk>
<jv>bar</jv>,
+ * <ja>@PathRemainder</ja> String <jv>remainder</jv>,
<jc>// Equivalent to @Path("/*")</jc>
+ * ) {...}
+ * </p>
+ *
+ * <p>
+ * This is functionally equivalent to using
<c><ja>@Path</ja>(<js>"/*"</js>)</c>, but provides a more intuitive name.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Path">@Path</a>
+ * </ul>
+ *
+ * <h5 class='topic'>Arguments and argument-types of client-side
@RemoteResource-annotated interfaces</h5>
+ * <p>
+ * Annotation applied to Java method arguments of interface proxies to denote
that they represent the path remainder on the request.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/PathRemainder">@PathRemainder</a>
+ * </ul>
+ *
+ * <h5 class='topic'>Methods and return types of server-side and client-side
@Request-annotated interfaces</h5>
+ * <p>
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Request">@Request</a>
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='ja'>{@link Path}
+ * </ul>
+ *
+ * @since 9.2.0
+ */
+@Documented
+@Target({PARAMETER,METHOD,TYPE,FIELD})
+@Retention(RUNTIME)
+@Inherited
+@Repeatable(PathRemainderAnnotation.Array.class)
+@ContextApply(PathRemainderAnnotation.Applier.class)
+public @interface PathRemainder {
+
+ /**
+ * Default value for this parameter.
+ *
+ * @return The annotation value.
+ */
+ String def() default "";
+
+ /**
+ * Optional description for the exposed API.
+ *
+ * @return The annotation value.
+ */
+ String[] description() default {};
+
+ /**
+ * Dynamically apply this annotation to the specified classes.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations">Dynamically
Applied Annotations</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] on() default {};
+
+ /**
+ * Dynamically apply this annotation to the specified classes.
+ *
+ * <p>
+ * Identical to {@link #on()} except allows you to specify class
objects instead of a strings.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations">Dynamically
Applied Annotations</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ Class<?>[] onClass() default {};
+
+ /**
+ * Specifies the {@link HttpPartParser} class used for parsing strings
to values.
+ *
+ * <p>
+ * Overrides for this part the part parser defined on the REST resource
which by default is {@link OpenApiParser}.
+ *
+ * @return The annotation value.
+ */
+ Class<? extends HttpPartParser> parser() default
HttpPartParser.Void.class;
+
+ /**
+ * <mk>schema</mk> field of the <a class='doclink'
href='https://swagger.io/specification/v2#parameterObject'>Swagger Parameter
Object</a>.
+ *
+ * <p>
+ * The schema defining the type used for parameter.
+ *
+ * <p>
+ * The {@link Schema @Schema} annotation can also be used standalone on
the parameter or type.
+ * Values specified on this field override values specified on the
type, and values specified on child types override values
+ * specified on parent types.
+ *
+ * <h5 class='section'>Used for:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Server-side schema-based parsing and parsing validation.
+ * <li>
+ * Server-side generated Swagger documentation.
+ * <li>
+ * Client-side schema-based serializing and serializing
validation.
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ Schema schema() default @Schema;
+
+ /**
+ * Specifies the {@link HttpPartSerializer} class used for serializing
values to strings.
+ *
+ * <p>
+ * Overrides for this part the part serializer defined on the REST
client which by default is {@link OpenApiSerializer}.
+ *
+ * @return The annotation value.
+ */
+ Class<? extends HttpPartSerializer> serializer() default
HttpPartSerializer.Void.class;
+}
+
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainderAnnotation.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainderAnnotation.java
new file mode 100644
index 0000000000..c8d790650f
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainderAnnotation.java
@@ -0,0 +1,288 @@
+/*
+ * 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.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+import static org.apache.juneau.common.utils.Utils.*;
+import static org.apache.juneau.internal.ArrayUtils.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.reflect.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Utility classes and methods for the {@link PathRemainder @PathRemainder}
annotation.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='ja'>{@link PathRemainder}
+ * <li class='ja'>{@link Path}
+ * </ul>
+ *
+ * @since 9.2.0
+ */
+public class PathRemainderAnnotation {
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Static
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /** Default value */
+ public static final PathRemainder DEFAULT = create().build();
+
+ /**
+ * Instantiates a new builder for this class.
+ *
+ * @return A new builder object.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /**
+ * Instantiates a new builder for this class.
+ *
+ * @param on The targets this annotation applies to.
+ * @return A new builder object.
+ */
+ public static Builder create(Class<?>...on) {
+ return create().on(on);
+ }
+
+ /**
+ * Instantiates a new builder for this class.
+ *
+ * @param on The targets this annotation applies to.
+ * @return A new builder object.
+ */
+ public static Builder create(String...on) {
+ return create().on(on);
+ }
+
+ /**
+ * Returns <jk>true</jk> if the specified annotation contains all
default values.
+ *
+ * @param a The annotation to check.
+ * @return <jk>true</jk> if the specified annotation contains all
default values.
+ */
+ public static boolean empty(PathRemainder a) {
+ return a == null || DEFAULT.equals(a);
+ }
+
+ /**
+ * Finds the default value from the specified list of annotations.
+ *
+ * @param pi The parameter.
+ * @return The last matching default value, or {@link Value#empty()} if
not found.
+ */
+ public static Value<String> findDef(ParamInfo pi) {
+ Value<String> n = Value.empty();
+ pi.forEachAnnotation(PathRemainder.class, x ->
isNotEmpty(x.def()), x -> n.set(x.def()));
+ return n;
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Builder
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Builder class.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='jm'>{@link
org.apache.juneau.BeanContext.Builder#annotations(Annotation...)}
+ * </ul>
+ */
+ public static class Builder extends
TargetedAnnotationTMFBuilder<Builder> {
+
+ Class<? extends HttpPartParser> parser =
HttpPartParser.Void.class;
+ Class<? extends HttpPartSerializer> serializer =
HttpPartSerializer.Void.class;
+ Schema schema = SchemaAnnotation.DEFAULT;
+ String def="";
+ String[] description = {};
+
+ /**
+ * Constructor.
+ */
+ protected Builder() {
+ super(PathRemainder.class);
+ }
+
+ /**
+ * Instantiates a new {@link PathRemainder @PathRemainder}
object initialized with this builder.
+ *
+ * @return A new {@link PathRemainder @PathRemainder} object.
+ */
+ public PathRemainder build() {
+ return new Impl(this);
+ }
+
+ /**
+ * Sets the {@link PathRemainder#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 PathRemainder#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 PathRemainder#parser} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder parser(Class<? extends HttpPartParser> value) {
+ this.parser = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link PathRemainder#schema} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder schema(Schema value) {
+ this.schema = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link PathRemainder#serializer} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder serializer(Class<? extends HttpPartSerializer>
value) {
+ this.serializer = value;
+ return this;
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Implementation
+
//-----------------------------------------------------------------------------------------------------------------
+
+ private static class Impl extends TargetedAnnotationTImpl implements
PathRemainder {
+
+ private final Class<? extends HttpPartParser> parser;
+ private final Class<? extends HttpPartSerializer> serializer;
+ private final String def;
+ private final String[] description;
+ private final Schema schema;
+
+ Impl(Builder b) {
+ super(b);
+ this.def = b.def;
+ this.description = copyOf(b.description);
+ this.parser = b.parser;
+ this.schema = b.schema;
+ this.serializer = b.serializer;
+ postConstruct();
+ }
+
+ @Override /* PathRemainder */
+ public String def() {
+ return def;
+ }
+
+ @Override /* PathRemainder */
+ public String[] description() {
+ return description;
+ }
+
+ @Override /* PathRemainder */
+ public Class<? extends HttpPartParser> parser() {
+ return parser;
+ }
+
+ @Override /* PathRemainder */
+ public Schema schema() {
+ return schema;
+ }
+
+ @Override /* PathRemainder */
+ public Class<? extends HttpPartSerializer> serializer() {
+ return serializer;
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Appliers
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Applies targeted {@link PathRemainder} annotations to a {@link
org.apache.juneau.BeanContext.Builder}.
+ */
+ public static class Applier extends
AnnotationApplier<PathRemainder,BeanContext.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public Applier(VarResolverSession vr) {
+ super(PathRemainder.class, BeanContext.Builder.class,
vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<PathRemainder> ai,
BeanContext.Builder b) {
+ PathRemainder a = ai.inner();
+ if (isEmptyArray(a.on(), a.onClass()))
+ return;
+ b.annotations(a);
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Other
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * A collection of {@link PathRemainder @PathRemainder annotations}.
+ */
+ @Documented
+ @Target({METHOD,TYPE})
+ @Retention(RUNTIME)
+ @Inherited
+ public static @interface Array {
+
+ /**
+ * The child annotations.
+ *
+ * @return The annotation value.
+ */
+ PathRemainder[] value();
+ }
+}
\ No newline at end of file
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 75ee3979cf..1801041b0a 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
@@ -746,6 +746,8 @@ public class HttpPartSchema {
apply((Query)a);
else if (a instanceof Path)
apply((Path)a);
+ else if (a instanceof PathRemainder)
+ apply((PathRemainder)a);
else if (a instanceof Response)
apply((Response)a);
else if (a instanceof StatusCode)
@@ -819,6 +821,22 @@ public class HttpPartSchema {
return this;
}
+ Builder apply(PathRemainder a) {
+ if (! SchemaAnnotation.empty(a.schema()))
+ apply(a.schema());
+ // PathRemainder is always "/*"
+ name("/*");
+ _default(a.def());
+ parser(a.parser());
+ serializer(a.serializer());
+
+ // Path remainder always allows empty value.
+ allowEmptyValue();
+ required(false);
+
+ return this;
+ }
+
Builder apply(Response a) {
allowEmptyValue(true);
apply(a.schema());
diff --git a/juneau-docs/docs/release-notes/9.2.0.md
b/juneau-docs/docs/release-notes/9.2.0.md
index 734b6ba71d..7c34125fd8 100644
--- a/juneau-docs/docs/release-notes/9.2.0.md
+++ b/juneau-docs/docs/release-notes/9.2.0.md
@@ -344,10 +344,51 @@ Major changes include:
- **Enhanced OpenAPI Compatibility**: Jakarta Validation constraints are now
seamlessly translated to OpenAPI schema properties, enabling automatic
documentation generation and client-side validation in tools like Swagger UI.
+#### @PathRemainder Annotation
+
+- **New Annotation**: Added `@PathRemainder` as an intuitive shortcut for
`@Path("/*")` to capture the path remainder in REST endpoints.
+
+ **Features**:
+ - More explicit and self-documenting than `@Path("/*")`
+ - Works seamlessly in both server-side REST methods and client-side remote
proxies
+ - Supports all `@Path` features: default values, custom parsers/serializers,
schema validation, OpenAPI documentation
+ - Automatically configured with `required=false` and `allowEmptyValue=true`
(path remainder is optional)
+
+ **Usage Example - Server Side**:
+ ```java
+ @RestGet("/files/*")
+ public File getFile(@PathRemainder String path) {
+ return new File(path);
+ }
+ ```
+
+ **Usage Example - Client Side**:
+ ```java
+ @Remote
+ public interface FileService {
+ @RemoteGet("/files/{+remainder}")
+ File getFile(@PathRemainder String remainder);
+ }
+ ```
+
+ **Implementation Details**:
+ - Created `PathRemainderArg` resolver for server-side parameter resolution
+ - Added support in `RemoteOperationArg` for client-side remote proxies
+ - Extended `HttpPartSchema.Builder` with `apply(PathRemainder)` method
+ - Comprehensive unit tests for server-side, including default values and
mixed parameters
+
+ **Documentation**:
+ - Updated `/docs/topics/09.03.04.PathVariables.md` with path remainder
examples
+ - Updated `/docs/topics/11.10.08.Path.md` for remote proxy usage
+
#### CSV Serializer - Object Swap Support
- **Full Object Swap Support**: The `CsvSerializer` now supports object swaps,
bringing it to feature parity with other Juneau serializers like JSON, XML, and
UON.
+ :::note
+ The `CsvParser` is not yet implemented. CSV parsing support will be added in
a future release, at which point swap support will be included.
+ :::
+
**Key Features**:
- **Bean Property Swaps**: Automatically applies swaps registered via
`.swaps()` to bean property values
- **Map Value Swaps**: Transforms map values using registered swaps before
CSV serialization
diff --git a/juneau-docs/docs/topics/09.03.04.PathVariables.md
b/juneau-docs/docs/topics/09.03.04.PathVariables.md
index 41db1736fb..0825cc47a0 100644
--- a/juneau-docs/docs/topics/09.03.04.PathVariables.md
+++ b/juneau-docs/docs/topics/09.03.04.PathVariables.md
@@ -28,3 +28,33 @@ Path variables resolved in parent resource paths are also
available to the child
:::note
All variables in the path must be specified or else the target will not
resolve and a `404` will result.
:::
+
+## Path Remainder
+
+The path remainder (the part matched by `/*`) can be captured using either <a
href="/site/apidocs/org/apache/juneau/http/annotation/Path.html"
target="_blank">@Path("/*")</a> or the more intuitive <a
href="/site/apidocs/org/apache/juneau/http/annotation/PathRemainder.html"
target="_blank">@PathRemainder</a> annotation.
+
+:::tip Example
+```java
+@Rest
+public class MyResource extends BasicRestServlet {
+
+ // Using @PathRemainder (preferred)
+ @RestGet("/files/*")
+ public File getFile(@PathRemainder String path) {
+ return new File(path);
+ }
+
+ // Equivalent using @Path("/*")
+ @RestPost("/upload/*")
+ public void uploadFile(@Path("/*") String path, @Content File file) {
+ ...
+ }
+}
+```
+:::
+
+The <a
href="/site/apidocs/org/apache/juneau/http/annotation/PathRemainder.html"
target="_blank">@PathRemainder</a> annotation supports the same features as <a
href="/site/apidocs/org/apache/juneau/http/annotation/Path.html"
target="_blank">@Path</a>, including:
+- Default values via `def()` property
+- Custom parsers and serializers
+- Schema validation
+- OpenAPI/Swagger documentation generation
diff --git a/juneau-docs/docs/topics/11.10.08.Path.md
b/juneau-docs/docs/topics/11.10.08.Path.md
index 7ddb48efb2..218dc338e0 100644
--- a/juneau-docs/docs/topics/11.10.08.Path.md
+++ b/juneau-docs/docs/topics/11.10.08.Path.md
@@ -52,8 +52,29 @@ Path arguments can be any of the following types:
See the link below for information about supported data types in OpenAPI
serialization.
+## Path Remainder
+
+For capturing the path remainder (the part matched by `/*`), you can use the
<a href="/site/apidocs/org/apache/juneau/http/annotation/PathRemainder.html"
target="_blank">@PathRemainder</a> annotation as a more intuitive alternative
to `@Path("/*")`.
+
+:::tip Example
+```java
+@Remote(path="/myproxy")
+public interface MyProxy {
+
+ // Using @PathRemainder (preferred)
+ @RemoteGet("/files/{+remainder}")
+ String getFile(@PathRemainder String remainder);
+
+ // Equivalent using @Path("/*")
+ @RemotePost("/upload/{+remainder}")
+ void uploadFile(@Path("/*") String remainder, @Content File file);
+}
+```
+:::
+
:::info See Also
- [OpenAPI Serializers](/docs/topics/OpenApiSerializers)
+- <a href="/site/apidocs/org/apache/juneau/http/annotation/PathRemainder.html"
target="_blank">@PathRemainder</a>
:::
\ No newline at end of file
diff --git
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteOperationArg.java
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteOperationArg.java
index ecf8789b8b..2614a416cc 100644
---
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteOperationArg.java
+++
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteOperationArg.java
@@ -47,6 +47,14 @@ public class RemoteOperationArg {
this.schema = schema;
}
+ RemoteOperationArg(int index, HttpPartType partType, HttpPartSchema
schema, String overrideName) {
+ this.index = index;
+ this.partType = partType;
+ this.serializer =
BeanCreator.of(HttpPartSerializer.class).type(schema.getSerializer()).execute();
+ // Create a new schema with the overridden name
+ this.schema =
HttpPartSchema.create().name(overrideName).build();
+ }
+
/**
* Returns the name of the HTTP part.
*
@@ -109,6 +117,11 @@ public class RemoteOperationArg {
return new RemoteOperationArg(i, QUERY,
HttpPartSchema.create(Query.class, mpi));
} else if (mpi.hasAnnotation(FormData.class)) {
return new RemoteOperationArg(i, FORMDATA,
HttpPartSchema.create(FormData.class, mpi));
+ } else if (mpi.hasAnnotation(PathRemainder.class)) {
+ // PathRemainder is equivalent to @Path("/*")
+ // Create with schema properties but override name to
"/*"
+ HttpPartSchema schema =
HttpPartSchema.create(PathRemainder.class, mpi);
+ return new RemoteOperationArg(i, PATH, schema, "/*");
} else if (mpi.hasAnnotation(Path.class)) {
return new RemoteOperationArg(i, PATH,
HttpPartSchema.create(Path.class, mpi));
} else if (mpi.hasAnnotation(Content.class)) {
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 0399f1aac4..a734502f8c 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -3198,6 +3198,7 @@ public class RestContext extends Context {
MethodArg.class,
ParserArg.class,
PathArg.class,
+ PathRemainderArg.class,
QueryArg.class,
ReaderParserArg.class,
RequestBeanArg.class,
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/PathRemainderArg.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/PathRemainderArg.java
new file mode 100644
index 0000000000..b3085a7110
--- /dev/null
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/PathRemainderArg.java
@@ -0,0 +1,102 @@
+/*
+ * 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.rest.arg;
+
+import static org.apache.juneau.http.annotation.PathRemainderAnnotation.*;
+
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.reflect.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.httppart.*;
+import org.apache.juneau.rest.util.*;
+
+/**
+ * Resolves method parameters annotated with {@link PathRemainder} on {@link
RestOp}-annotated Java methods.
+ *
+ * <p>
+ * This is a specialized version of {@link PathArg} for the path remainder
(the part matched by {@code /*}).
+ * It's functionally equivalent to using {@code @Path("/*")}, but provides a
more intuitive annotation name.
+ *
+ * <p>
+ * The parameter value is resolved using:
+ * <p class='bjava'>
+ * <jv>opSession</jv>
+ * .{@link RestOpSession#getRequest() getRequest}()
+ * .{@link RestRequest#getPathParams() getPathParams}()
+ * .{@link RequestPathParams#get(String) get}(<js>"/*"</js>)
+ * .{@link RequestPathParam#as(Class) as}(<jv>type</jv>);
+ * </p>
+ *
+ * <p>
+ * {@link HttpPartSchema schema} is derived from the {@link PathRemainder}
annotation.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method
Parameters</a>
+ * <li class='ja'>{@link PathRemainder}
+ * <li class='jc'>{@link PathArg}
+ * </ul>
+ *
+ * @since 9.2.0
+ */
+public class PathRemainderArg implements RestOpArg {
+ private final HttpPartParser partParser;
+ private final HttpPartSchema schema;
+ private final String def;
+ private final Type type;
+
+ /**
+ * Static creator.
+ *
+ * @param paramInfo The Java method parameter being resolved.
+ * @param annotations The annotations to apply to any new part parsers.
+ * @param pathMatcher Path matcher for the specified method (not used,
but included for BeanStore compatibility).
+ * @return A new {@link PathRemainderArg}, or <jk>null</jk> if the
parameter is not annotated with {@link PathRemainder}.
+ */
+ public static PathRemainderArg create(ParamInfo paramInfo,
AnnotationWorkList annotations, UrlPathMatcher pathMatcher) {
+ if (paramInfo.hasAnnotation(PathRemainder.class) ||
paramInfo.getParameterType().hasAnnotation(PathRemainder.class))
+ return new PathRemainderArg(paramInfo, annotations);
+ return null;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param paramInfo The Java method parameter being resolved.
+ * @param annotations The annotations to apply to any new part parsers.
+ */
+ protected PathRemainderArg(ParamInfo paramInfo, AnnotationWorkList
annotations) {
+ this.def = findDef(paramInfo).orElse(null);
+ this.type = paramInfo.getParameterType().innerType();
+ this.schema = HttpPartSchema.create(PathRemainder.class,
paramInfo);
+ Class<? extends HttpPartParser> pp = schema.getParser();
+ this.partParser = pp != null ?
HttpPartParser.creator().type(pp).apply(annotations).create() : null;
+ }
+
+ @Override /* RestOpArg */
+ public Object resolve(RestOpSession opSession) throws Exception {
+ RestRequest req = opSession.getRequest();
+ HttpPartParserSession ps = partParser == null ?
req.getPartParserSession() : partParser.getPartSession();
+ // The path remainder is stored under the name "/*"
+ return
req.getPathParams().get("/*").parser(ps).schema(schema).def(def).as(type).orElse(null);
+ }
+}
+
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/PathRemainder_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/PathRemainder_Test.java
index 0eb82e9e4c..df18f4d706 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/PathRemainder_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/PathRemainder_Test.java
@@ -132,4 +132,158 @@ class PathRemainder_Test extends TestBase {
.assertStatus(200)
.assertContent("[{a:1,b:'foo'}]");
}
+
+
//------------------------------------------------------------------------------------------------------------------
+ // @PathRemainder annotation tests
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class C {
+ @RestOp(path="/a/*")
+ public String a(@PathRemainder String remainder) {
+ return ""+remainder;
+ }
+ @RestGet(path="/b/*")
+ public String b(@PathRemainder String remainder) {
+ return ""+remainder;
+ }
+ @RestPut(path="/c/*")
+ public String c(@PathRemainder String remainder) {
+ return ""+remainder;
+ }
+ @RestPost(path="/d/*")
+ public String d(@PathRemainder String remainder) {
+ return ""+remainder;
+ }
+ @RestDelete(path="/e/*")
+ public String e(@PathRemainder String remainder) {
+ return ""+remainder;
+ }
+ }
+
+ @Test void c01_pathRemainderAnnotation() throws Exception {
+ var c = MockRestClient.build(C.class);
+
+ // Test that @PathRemainder works identically to @Path("/*")
+ c.get("/a").run().assertContent("null");
+ c.get("/a/").run().assertContent("");
+ c.get("/a/foo").run().assertContent("foo");
+ c.get("/a/foo/bar").run().assertContent("foo/bar");
+
+ c.get("/b").run().assertContent("null");
+ c.get("/b/").run().assertContent("");
+ c.get("/b/foo").run().assertContent("foo");
+ c.get("/b/foo/bar").run().assertContent("foo/bar");
+
+ c.put("/c").run().assertContent("null");
+ c.put("/c/").run().assertContent("");
+ c.put("/c/foo").run().assertContent("foo");
+ c.put("/c/foo/bar").run().assertContent("foo/bar");
+
+ c.post("/d").run().assertContent("null");
+ c.post("/d/").run().assertContent("");
+ c.post("/d/foo").run().assertContent("foo");
+ c.post("/d/foo/bar").run().assertContent("foo/bar");
+
+ c.delete("/e").run().assertContent("null");
+ c.delete("/e/").run().assertContent("");
+ c.delete("/e/foo").run().assertContent("foo");
+ c.delete("/e/foo/bar").run().assertContent("foo/bar");
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // @PathRemainder with Optional and complex types
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Rest(serializers=Json5Serializer.class)
+ public static class D {
+ @RestGet(path="/a/*")
+ public Object a(@PathRemainder Optional<Integer> f1) {
+ assertNotNull(f1);
+ return f1;
+ }
+ @RestPut(path="/b/*")
+ public Object b(@PathRemainder Optional<ABean> f1) {
+ assertNotNull(f1);
+ return f1;
+ }
+ @RestPost(path="/c/*")
+ public Object c(@PathRemainder Optional<List<ABean>> f1) {
+ assertNotNull(f1);
+ return f1;
+ }
+ @RestDelete(path="/d/*")
+ public Object d(@PathRemainder List<Optional<ABean>> f1) {
+ return f1;
+ }
+ }
+
+ @Test void d01_pathRemainderWithOptional() throws Exception {
+ var d = MockRestClient.buildJson(D.class);
+ d.get("/a/123")
+ .run()
+ .assertStatus(200)
+ .assertContent("123");
+ d.put("/b/a=1,b=foo")
+ .run()
+ .assertStatus(200)
+ .assertContent("{a:1,b:'foo'}");
+ d.post("/c/@((a=1,b=foo))")
+ .run()
+ .assertStatus(200)
+ .assertContent("[{a:1,b:'foo'}]");
+ d.delete("/d/@((a=1,b=foo))")
+ .run()
+ .assertStatus(200)
+ .assertContent("[{a:1,b:'foo'}]");
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // @PathRemainder with mixed path parameters
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class E {
+ @RestGet(path="/a/{foo}/{bar}/*")
+ public String a(@Path("foo") String foo, @Path("bar") int bar,
@PathRemainder String remainder) {
+ return "foo="+foo+",bar="+bar+",remainder="+remainder;
+ }
+ @RestPost(path="/b/{id}/*")
+ public String b(@Path("id") String id, @PathRemainder String
remainder) {
+ return "id="+id+",remainder="+remainder;
+ }
+ }
+
+ @Test void e01_pathRemainderWithOtherPathParams() throws Exception {
+ var e = MockRestClient.build(E.class);
+ e.get("/a/x/123/extra/path")
+ .run()
+ .assertContent("foo=x,bar=123,remainder=extra/path");
+ e.get("/a/hello/456")
+ .run()
+ .assertContent("foo=hello,bar=456,remainder=null");
+ e.post("/b/myId/more/stuff")
+ .run()
+ .assertContent("id=myId,remainder=more/stuff");
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // @PathRemainder with default values
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class F {
+ @RestGet(path="/a/*")
+ public String a(@PathRemainder(def="defaultValue") String
remainder) {
+ return ""+remainder;
+ }
+ }
+
+ @Test void f01_pathRemainderWithDefault() throws Exception {
+ var f = MockRestClient.build(F.class);
+ f.get("/a").run().assertContent("defaultValue");
+ f.get("/a/").run().assertContent("");
+ f.get("/a/custom").run().assertContent("custom");
+ }
+
}
\ No newline at end of file
diff --git a/scripts/README-check-fluent-setter-overrides.md
b/scripts/README-check-fluent-setter-overrides.md
new file mode 100644
index 0000000000..04dd6857a9
--- /dev/null
+++ b/scripts/README-check-fluent-setter-overrides.md
@@ -0,0 +1,164 @@
+# Check Fluent Setter Overrides Script
+
+## Purpose
+
+This script scans the Juneau codebase to find missing fluent setter overrides
in subclasses.
+
+## Problem
+
+Juneau uses fluent-style setters extensively for method chaining:
+
+```java
+public class X {
+ public X setY(Y y) {
+ this.y = y;
+ return this;
+ }
+}
+```
+
+When a class extends another class with fluent setters, it should override
those setters to return the correct subclass type:
+
+```java
+public class X2 extends X {
+ @Override
+ public X2 setY(Y y) {
+ super.setY(y);
+ return this;
+ }
+}
+```
+
+Without these overrides, method chaining breaks:
+
+```java
+// Without override - compiler error!
+X2 obj = new X2()
+ .setY(y) // Returns X, not X2
+ .setX2SpecificMethod(); // Error: X doesn't have this method
+
+// With proper override - works!
+X2 obj = new X2()
+ .setY(y) // Returns X2
+ .setX2SpecificMethod(); // OK
+```
+
+## Usage
+
+### Basic Usage
+
+Run from the Juneau root directory:
+
+```bash
+python3 scripts/check-fluent-setter-overrides.py
+```
+
+Or run directly (script is executable):
+
+```bash
+./scripts/check-fluent-setter-overrides.py
+```
+
+### Output
+
+The script will:
+1. Scan all Java files in the source tree
+2. Identify classes and their inheritance relationships
+3. Find fluent setter methods (methods that return `this`)
+4. Check if subclasses properly override these setters
+5. Report any missing overrides grouped by class
+
+Example output:
+
+```
+Juneau Fluent Setter Override Checker
+==================================================
+
+Scanning for Java files...
+Found 2847 Java files
+
+Extracting class information...
+Found 1523 classes
+Found 3421 fluent setter methods
+
+Building class hierarchy...
+
+Checking for missing fluent setter overrides...
+
+MISSING OVERRIDES (23 found):
+==================================================
+
+Class: RestClientBuilder
+ File:
juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java
+ Missing 5 override(s):
+ - debug(Enablement)
+ From parent: BeanContextBuilder
+ - locale(Locale)
+ From parent: BeanContextBuilder
+ - mediaType(MediaType)
+ From parent: BeanContextBuilder
+ - timeZone(TimeZone)
+ From parent: BeanContextBuilder
+ - beansRequireDefaultConstructor()
+ From parent: BeanContextBuilder
+
+Class: HtmlDocSerializerBuilder
+ File:
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java
+ Missing 3 override(s):
+ - addBeanTypes()
+ From parent: HtmlSerializerBuilder
+ - detectRecursions()
+ From parent: SerializerBuilder
+ - ignoreRecursions()
+ From parent: SerializerBuilder
+
+==================================================
+Total missing overrides: 23
+
+Note: These are informational and do not fail the build.
+Consider adding these overrides to maintain fluent API consistency.
+```
+
+## What the Script Checks
+
+The script identifies fluent setters by looking for:
+- Public methods that return the class type (e.g., `public X setFoo(...)`)
+- Methods that contain `return this;` in their body
+- Methods in parent classes that should be overridden in subclasses
+
+## What to Do with Results
+
+When the script reports missing overrides:
+
+1. **Review the findings** - Not all reported methods may need overrides
+2. **Add missing overrides** - For methods that should be overridden:
+
+```java
+@Override
+public SubClass methodName(ParamType param) {
+ super.methodName(param);
+ return this;
+}
+```
+
+3. **Consider fluent API design** - Ensure method chaining works correctly
across the inheritance hierarchy
+
+## Limitations
+
+- The script uses regex-based parsing (not a full Java parser)
+- May not catch all edge cases (inner classes, complex generics, etc.)
+- Reports are informational only - manual review is recommended
+- Does not validate that overrides are implemented correctly
+
+## Integration
+
+This script can be run:
+- Manually during development
+- As part of code review process
+- In CI/CD pipelines (informational only)
+- Before releases to ensure API consistency
+
+## Exit Code
+
+The script always exits with code 0 (success) to avoid failing builds. Results
are informational only.
+
diff --git a/scripts/check-fluent-setter-overrides.py
b/scripts/check-fluent-setter-overrides.py
new file mode 100755
index 0000000000..a5260f27b7
--- /dev/null
+++ b/scripts/check-fluent-setter-overrides.py
@@ -0,0 +1,273 @@
+#!/usr/bin/env python3
+#
***************************************************************************************************************************
+# * 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.
*
+#
***************************************************************************************************************************
+"""
+Script to check for missing fluent setter overrides in the Juneau codebase.
+
+This script:
+1. Scans all Java files in the source tree
+2. Identifies classes and their parent classes
+3. Finds fluent setter methods (methods that return 'this')
+4. Checks if subclasses override these setters with the correct return type
+5. Reports any missing overrides
+
+A fluent setter is a method that:
+- Starts with 'set' or is a builder-style method
+- Returns the class type (for method chaining)
+- Is public
+"""
+
+import os
+import re
+import sys
+from pathlib import Path
+from collections import defaultdict
+
+class JavaClass:
+ """Represents a Java class with its metadata."""
+ def __init__(self, name, file_path, extends=None, package=None):
+ self.name = name
+ self.file_path = file_path
+ self.extends = extends
+ self.package = package
+ self.fluent_setters = [] # List of (method_name, params, return_type)
+ self.overridden_methods = set() # Set of method signatures that are
overridden
+
+ def add_fluent_setter(self, method_name, params, return_type):
+ """Add a fluent setter method."""
+ self.fluent_setters.append({
+ 'name': method_name,
+ 'params': params,
+ 'return_type': return_type
+ })
+
+ def add_overridden_method(self, method_name, params):
+ """Add an overridden method."""
+ signature = f"{method_name}({params})"
+ self.overridden_methods.add(signature)
+
+ def get_full_name(self):
+ """Get the fully qualified class name."""
+ if self.package:
+ return f"{self.package}.{self.name}"
+ return self.name
+
+def extract_package(content):
+ """Extract package name from Java file content."""
+ match = re.search(r'^\s*package\s+([\w.]+)\s*;', content, re.MULTILINE)
+ return match.group(1) if match else None
+
+def extract_class_info(file_path):
+ """Extract class information from a Java file."""
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ package = extract_package(content)
+
+ # Find class declarations (public class X extends Y)
+ class_pattern = re.compile(
+
r'^\s*public\s+(?:static\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+([\w.<>,
]+?))?(?:\s+implements\s+[\w.<>, ]+?)?\s*\{',
+ re.MULTILINE
+ )
+
+ classes = []
+ for match in class_pattern.finditer(content):
+ class_name = match.group(1)
+ extends = match.group(2).strip() if match.group(2) else None
+
+ # Clean up extends (remove generics for simplicity)
+ if extends:
+ extends = re.sub(r'<.*?>', '', extends).strip()
+
+ java_class = JavaClass(class_name, file_path, extends, package)
+
+ # Find fluent setters in this class
+ # Pattern: public ClassName methodName(...) { ... return this; }
+ # We look for methods that return the class type
+ method_pattern = re.compile(
+
rf'^\s*(?:@Override\s+)?public\s+{re.escape(class_name)}\s+(\w+)\s*\((.*?)\)\s*\{{',
+ re.MULTILINE
+ )
+
+ for method_match in method_pattern.finditer(content):
+ method_name = method_match.group(1)
+ params = method_match.group(2).strip()
+
+ # Check if this method returns 'this'
+ # Look ahead to see if there's a 'return this;' in the method
body
+ method_start = method_match.end()
+ # Find the matching closing brace (simplified - just look for
return this)
+ method_body_sample = content[method_start:method_start + 500]
+ if 'return this;' in method_body_sample or 'return this' in
method_body_sample:
+ java_class.add_fluent_setter(method_name, params,
class_name)
+
+ # Find overridden methods
+ override_pattern = re.compile(
+ r'@Override[^\n]*\n\s*public\s+\w+\s+(\w+)\s*\((.*?)\)',
+ re.MULTILINE
+ )
+
+ for override_match in override_pattern.finditer(content):
+ method_name = override_match.group(1)
+ params = override_match.group(2).strip()
+ java_class.add_overridden_method(method_name, params)
+
+ classes.append(java_class)
+
+ return classes
+
+ except Exception as e:
+ print(f"ERROR: Failed to process {file_path}: {e}", file=sys.stderr)
+ return []
+
+def find_java_files(source_dir):
+ """Find all Java files in the source tree."""
+ java_files = []
+
+ for root, dirs, files in os.walk(source_dir):
+ # Skip certain directories
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d not in
{'target', 'node_modules', 'build', 'dist'}]
+
+ for file in files:
+ if file.endswith('.java'):
+ java_files.append(Path(root) / file)
+
+ return java_files
+
+def build_class_hierarchy(classes):
+ """Build a hierarchy of classes by name."""
+ class_map = {} # Maps class name to JavaClass objects (may have
duplicates)
+
+ for java_class in classes:
+ class_name = java_class.name
+ if class_name not in class_map:
+ class_map[class_name] = []
+ class_map[class_name].append(java_class)
+
+ return class_map
+
+def check_missing_overrides(classes, class_map):
+ """Check for missing fluent setter overrides."""
+ missing_overrides = []
+
+ for java_class in classes:
+ if not java_class.extends:
+ continue
+
+ parent_name = java_class.extends
+
+ # Find parent class
+ if parent_name not in class_map:
+ continue
+
+ # Get all parent classes with this name (there may be multiple in
different packages)
+ parent_classes = class_map[parent_name]
+
+ # Check each parent class
+ for parent_class in parent_classes:
+ # Check each fluent setter in the parent
+ for setter in parent_class.fluent_setters:
+ method_name = setter['name']
+ params = setter['params']
+ signature = f"{method_name}({params})"
+
+ # Check if this method is overridden in the child class
+ if signature not in java_class.overridden_methods:
+ # Also check if the child has this as a fluent setter
+ # (it might define it without @Override annotation)
+ has_fluent = False
+ for child_setter in java_class.fluent_setters:
+ if child_setter['name'] == method_name and
child_setter['params'] == params:
+ has_fluent = True
+ break
+
+ if not has_fluent:
+ missing_overrides.append({
+ 'child_class': java_class.name,
+ 'child_file': str(java_class.file_path),
+ 'parent_class': parent_class.name,
+ 'parent_file': str(parent_class.file_path),
+ 'method_name': method_name,
+ 'method_params': params,
+ 'method_signature': signature
+ })
+
+ return missing_overrides
+
+def main():
+ """Main entry point."""
+ # Get the script directory (should be /juneau/scripts)
+ script_dir = Path(__file__).parent
+ juneau_root = script_dir.parent
+
+ print("Juneau Fluent Setter Override Checker")
+ print("=" * 50)
+
+ # Find all Java files
+ print("\nScanning for Java files...")
+ java_files = find_java_files(juneau_root)
+ print(f"Found {len(java_files)} Java files")
+
+ # Extract class information
+ print("\nExtracting class information...")
+ all_classes = []
+ for java_file in java_files:
+ classes = extract_class_info(java_file)
+ all_classes.extend(classes)
+
+ print(f"Found {len(all_classes)} classes")
+
+ # Count fluent setters
+ total_fluent_setters = sum(len(c.fluent_setters) for c in all_classes)
+ print(f"Found {total_fluent_setters} fluent setter methods")
+
+ # Build class hierarchy
+ print("\nBuilding class hierarchy...")
+ class_map = build_class_hierarchy(all_classes)
+
+ # Check for missing overrides
+ print("\nChecking for missing fluent setter overrides...")
+ missing = check_missing_overrides(all_classes, class_map)
+
+ # Report results
+ if missing:
+ print(f"\nMISSING OVERRIDES ({len(missing)} found):")
+ print("=" * 50)
+
+ # Group by child class for better readability
+ by_class = defaultdict(list)
+ for item in missing:
+ by_class[item['child_class']].append(item)
+
+ for child_class, items in sorted(by_class.items()):
+ print(f"\nClass: {child_class}")
+ print(f" File: {items[0]['child_file']}")
+ print(f" Missing {len(items)} override(s):")
+
+ for item in items:
+ print(f" - {item['method_name']}({item['method_params']})")
+ print(f" From parent: {item['parent_class']}")
+
+ print(f"\n{'=' * 50}")
+ print(f"Total missing overrides: {len(missing)}")
+ print("\nNote: These are informational and do not fail the build.")
+ print("Consider adding these overrides to maintain fluent API
consistency.")
+ else:
+ print("\n✓ All fluent setters are properly overridden!")
+
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
+