You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2022/08/13 16:22:02 UTC
[juneau] branch jbFixRestNpe updated: Fix bug in @RemoteX annotations.
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch jbFixRestNpe
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/jbFixRestNpe by this push:
new 146cdccf0 Fix bug in @RemoteX annotations.
146cdccf0 is described below
commit 146cdccf0896a0bc2533c5baa2f396d860ea842d
Author: JamesBognar <ja...@salesforce.com>
AuthorDate: Sat Aug 13 12:21:41 2022 -0400
Fix bug in @RemoteX annotations.
---
.../org/apache/juneau/reflect/AnnotationInfo.java | 10 +
.../rest/client/remote/RemoteOperationMeta.java | 5 +-
.../org/apache/juneau/http/remote/RemotePatch.java | 125 ++++
.../apache/juneau/rest/annotation/RestOptions.java | 727 +++++++++++++++++++++
.../rest/annotation/RestOptionsAnnotation.java | 538 +++++++++++++++
.../remote/Remote_FormDataAnnotation_Test.java | 106 +--
.../org/apache/juneau/http/remote/Remote_Test.java | 103 +++
7 files changed, 1559 insertions(+), 55 deletions(-)
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/AnnotationInfo.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/AnnotationInfo.java
index a328cb925..02f571294 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/AnnotationInfo.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/AnnotationInfo.java
@@ -148,6 +148,16 @@ public final class AnnotationInfo<T extends Annotation> {
return a;
}
+ /**
+ * Returns the class name of the annotation.
+ *
+ * @return The simple class name of the annotation.
+ */
+ public String getName() {
+ return a.annotationType().getSimpleName();
+ }
+
+
/**
* Converts this object to a readable JSON object for debugging purposes.
*
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 2b22594e3..fdd43253a 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
@@ -92,7 +92,8 @@ public class RemoteOperationMeta {
al = mi.getReturnType().unwrap(Value.class,Optional.class).getAnnotationList(REMOTE_OP_GROUP);
Value<String> _httpMethod = Value.empty(), _path = Value.empty();
- al.forEachValue(String.class, "method", NOT_EMPTY, x -> _httpMethod.set(x.trim()));
+ al.stream().map(x -> x.getName().substring(6).toUpperCase()).filter(x -> ! x.equals("OP")).forEach(x -> _httpMethod.set(x));
+ al.forEachValue(String.class, "method", NOT_EMPTY, x -> _httpMethod.set(x.trim().toUpperCase()));
al.forEachValue(String.class, "path", NOT_EMPTY, x-> _path.set(x.trim()));
httpMethod = _httpMethod.orElse("").trim();
path = _path.orElse("").trim();
@@ -125,7 +126,7 @@ public class RemoteOperationMeta {
if (! isOneOf(httpMethod, "DELETE", "GET", "POST", "PUT", "OPTIONS", "HEAD", "CONNECT", "TRACE", "PATCH"))
throw new RemoteMetadataException(m,
- "Invalid value specified for @RemoteOp(httpMethod) annotation. Valid values are [DELTE,GET,POST,PUT,OPTIONS,HEAD,CONNECT,TRACE,PATCH].");
+ "Invalid value specified for @RemoteOp(httpMethod) annotation: '"+httpMethod+"'. Valid values are [DELETE,GET,POST,PUT,OPTIONS,HEAD,CONNECT,TRACE,PATCH].");
methodReturn = new RemoteOperationReturn(mi);
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/remote/RemotePatch.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/remote/RemotePatch.java
new file mode 100644
index 000000000..edf2e20a0
--- /dev/null
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/remote/RemotePatch.java
@@ -0,0 +1,125 @@
+// ***************************************************************************************************************************
+// * 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 java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.io.*;
+import java.lang.annotation.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.annotation.*;
+
+/**
+ * Annotation applied to Java methods on REST proxy interface classes.
+ *
+ * <p>
+ * Note that this annotation is optional if you do not need to override any of the values.
+ *
+ * <ul class='seealso'>
+ * <li class='link'>{@doc jrc.Proxies}
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+@Documented
+@Target(METHOD)
+@Retention(RUNTIME)
+@Inherited
+@AnnotationGroup(RemoteOp.class)
+public @interface RemotePatch {
+
+ /**
+ * REST service path.
+ *
+ * <p>
+ * If you do not specify a path, then the path is inferred from the Java method name.
+ *
+ * <h5 class='figure'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// PATCH /pet</jc>
+ * <ja>@RemotePatch</ja>
+ * <jk>public void</jk> patchPet(...);
+ * </p>
+ *
+ * <p>
+ * Note that you can also use {@link #value()} to specify the path in shortened form.
+ *
+ * <ul class='values'>
+ * <li>An absolute URL.
+ * <li>A relative URL interpreted as relative to the root URL defined on the <c>RestClient</c> and/or {@link Remote#path()}.
+ * <li>No path.
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String path() default "";
+
+ /**
+ * The value the remote method returns.
+ *
+ * <ul class='values'>
+ * <li>
+ * {@link RemoteReturn#BODY} (default) - The body of the HTTP response converted to a POJO.
+ * <br>The return type on the Java method can be any of the following:
+ * <ul class='spaced-list'>
+ * <li>
+ * <jk>void</jk> - Don't parse any response. Note that the method will still throw an exception if an
+ * error HTTP status is returned.
+ * <li>
+ * Any parsable POJO - The body of the response will be converted to the POJO using the parser defined
+ * on the <c>RestClient</c>.
+ * <li>
+ * Any POJO annotated with the {@link Response @Response} annotation.
+ * This allows for response beans to be used which also allows for OpenAPI-based parsing and validation.
+ * <li>
+ * <c>HttpResponse</c> - Returns the raw <c>HttpResponse</c> returned by the inner
+ * <c>HttpClient</c>.
+ * <li>
+ * {@link Reader} - Returns access to the raw reader of the response.
+ * <li>
+ * {@link InputStream} - Returns access to the raw input stream of the response.
+ * </ul>
+ * <li>
+ * {@link RemoteReturn#STATUS} - The HTTP status code on the response.
+ * <br>The return type on the Java method can be any of the following:
+ * <ul>
+ * <li><jk>int</jk>/<c>Integer</c> - The HTTP response code.
+ * <li><jk>boolean</jk>/<c>Boolean</c> - <jk>true</jk> if the response code is <c><400</c>
+ * </ul>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ RemoteReturn returns() default RemoteReturn.BODY;
+
+ /**
+ * REST path.
+ *
+ * <p>
+ * Can be used to provide a shortened form for the {@link #path()} value.
+ *
+ * <p>
+ * The following examples are considered equivalent.
+ * <p class='bjava'>
+ * <jc>// Normal form</jc>
+ * <ja>@RemotePatch</ja>(path=<js>"/{propertyName}"</js>)
+ *
+ * <jc>// Shortened form</jc>
+ * <ja>@RemotePatch</ja>(<js>"/{propertyName}"</js>)
+ * </p>
+ *
+ * @return The annotation value.
+ */
+ String value() default "";
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestOptions.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestOptions.java
new file mode 100644
index 000000000..d235a33fe
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestOptions.java
@@ -0,0 +1,727 @@
+// ***************************************************************************************************************************
+// * 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.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+import java.nio.charset.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.converter.*;
+import org.apache.juneau.rest.guard.*;
+import org.apache.juneau.rest.httppart.*;
+import org.apache.juneau.rest.matcher.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.rest.swagger.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.dto.swagger.*;
+import org.apache.juneau.encoders.*;
+
+/**
+ * Identifies a REST OPTIONS operation Java method on a {@link RestServlet} implementation class.
+ *
+ * <p>
+ * This is a specialized subtype of <c><ja>{@link RestOp @RestOp}(method=<jsf>OPTIONS</jsf>)</c>.
+ *
+ * <ul class='seealso'>
+ * <li class='link'>{@doc jrs.RestOpAnnotatedMethods}
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+@Target(METHOD)
+@Retention(RUNTIME)
+@Inherited
+@ContextApply(RestGetAnnotation.RestOpContextApply.class)
+@AnnotationGroup(RestOp.class)
+public @interface RestOptions {
+
+ /**
+ * Specifies whether this method can be called based on the client version.
+ *
+ * <p>
+ * The client version is identified via the HTTP request header identified by
+ * {@link Rest#clientVersionHeader() @Rest(clientVersionHeader)} which by default is <js>"Client-Version"</js>.
+ *
+ * <p>
+ * This is a specialized kind of {@link RestMatcher} that allows you to invoke different Java methods for the same
+ * method/path based on the client version.
+ *
+ * <p>
+ * The format of the client version range is similar to that of OSGi versions.
+ *
+ * <p>
+ * In the following example, the Java methods are mapped to the same HTTP method and URL <js>"/foobar"</js>.
+ * <p class='bjava'>
+ * <jc>// Call this method if Client-Version is at least 2.0.
+ * // Note that this also matches 2.0.1.</jc>
+ * <ja>@RestOptions</ja>(path=<js>"/foobar"</js>, clientVersion=<js>"2.0"</js>)
+ * <jk>public</jk> Object method1() {...}
+ *
+ * <jc>// Call this method if Client-Version is at least 1.1, but less than 2.0.</jc>
+ * <ja>@RestOptions</ja>(path=<js>"/foobar"</js>, clientVersion=<js>"[1.1,2.0)"</js>)
+ * <jk>public</jk> Object method2() {...}
+ *
+ * <jc>// Call this method if Client-Version is less than 1.1.</jc>
+ * <ja>@RestOptions</ja>(path=<js>"/foobar"</js>, clientVersion=<js>"[0,1.1)"</js>)
+ * <jk>public</jk> Object method3() {...}
+ * </p>
+ *
+ * <p>
+ * It's common to combine the client version with transforms that will convert new POJOs into older POJOs for
+ * backwards compatibility.
+ * <p class='bjava'>
+ * <jc>// Call this method if Client-Version is at least 2.0.</jc>
+ * <ja>@RestOptions</ja>(path=<js>"/foobar"</js>, clientVersion=<js>"2.0"</js>)
+ * <jk>public</jk> NewPojo newMethod() {...}
+ *
+ * <jc>// Call this method if Client-Version is at least 1.1, but less than 2.0.</jc>
+ * <ja>@RestOptions</ja>(path=<js>"/foobar"</js>, clientVersion=<js>"[1.1,2.0)"</js>)
+ * <ja>@BeanConfig(swaps=NewToOldSwap.<jk>class</jk>)
+ * <jk>public</jk> NewPojo oldMethod() {
+ * <jk>return</jk> newMethod();
+ * }
+ * </p>
+ *
+ * <p>
+ * Note that in the previous example, we're returning the exact same POJO, but using a transform to convert it into
+ * an older form.
+ * The old method could also just return back a completely different object.
+ * The range can be any of the following:
+ * <ul>
+ * <li><js>"[0,1.0)"</js> = Less than 1.0. 1.0 and 1.0.0 does not match.
+ * <li><js>"[0,1.0]"</js> = Less than or equal to 1.0. Note that 1.0.1 will match.
+ * <li><js>"1.0"</js> = At least 1.0. 1.0 and 2.0 will match.
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestContext.Builder#clientVersionHeader(String)}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String clientVersion() default "";
+
+ /**
+ * Class-level response converters.
+ *
+ * <p>
+ * Associates one or more {@link RestConverter converters} with this method.
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestOpContext.Builder#converters()} - Registering converters with REST resources.
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ Class<? extends RestConverter>[] converters() default {};
+
+ /**
+ * Enable debug mode.
+ *
+ * <p>
+ * Enables the following:
+ * <ul class='spaced-list'>
+ * <li>
+ * HTTP request/response bodies are cached in memory for logging purposes.
+ * <li>
+ * Request/response messages are automatically logged.
+ * </ul>
+ *
+ * <ul class='values'>
+ * <li><js>"true"</js> - Debug is enabled for all requests.
+ * <li><js>"false"</js> - Debug is disabled for all requests.
+ * <li><js>"conditional"</js> - Debug is enabled only for requests that have a <c class='snippet'>Debug: true</c> header.
+ * <li><js>""</js> (or anything else) - Debug mode is inherited from class.
+ * </ul>
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestContext.Builder#debugEnablement()}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String debug() default "";
+
+ /**
+ * Default <c>Accept</c> header.
+ *
+ * <p>
+ * The default value for the <c>Accept</c> header if not specified on a request.
+ *
+ * <p>
+ * This is a shortcut for using {@link #defaultRequestHeaders()} for just this specific header.
+ *
+ * @return The annotation value.
+ */
+ String defaultAccept() default "";
+
+ /**
+ * Default character encoding.
+ *
+ * <p>
+ * The default character encoding for the request and response if not specified on the request.
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$S{mySystemProperty}"</js>).
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestContext.Builder#defaultCharset(Charset)}
+ * <li class='jm'>{@link org.apache.juneau.rest.RestOpContext.Builder#defaultCharset(Charset)}
+ * <li class='ja'>{@link Rest#defaultCharset}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String defaultCharset() default "";
+
+ /**
+ * Specifies default values for query parameters.
+ *
+ * <p>
+ * Strings are of the format <js>"name=value"</js>.
+ *
+ * <p>
+ * Affects values returned by {@link RestRequest#getQueryParam(String)} when the parameter is not present on the request.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <ja>@RestOptions</ja>(path=<js>"/*"</js>, defaultRequestQueryData={<js>"foo=bar"</js>})
+ * <jk>public</jk> String doGet(<ja>@Query</ja>(<js>"foo"</js>) String <jv>foo</jv>) {...}
+ * </p>
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * You can use either <js>':'</js> or <js>'='</js> as the key/value delimiter.
+ * <li class='note'>
+ * Key and value is trimmed of whitespace.
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$S{mySystemProperty}"</js>).
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] defaultRequestQueryData() default {};
+
+ /**
+ * Default request attributes.
+ *
+ * <p>
+ * Specifies default values for request attributes if they're not already set on the request.
+ *
+ * <p>
+ * Affects values returned by the following methods:
+ * <ul>
+ * <li class='jm'>{@link RestRequest#getAttribute(String)}.
+ * <li class='jm'>{@link RestRequest#getAttributes()}.
+ * </ul>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Defined via annotation resolving to a config file setting with default value.</jc>
+ * <ja>@Rest</ja>(defaultRequestAttributes={<js>"Foo=bar"</js>, <js>"Baz: $C{REST/myAttributeValue}"</js>})
+ * <jk>public class</jk> MyResource {
+ *
+ * <jc>// Override at the method level.</jc>
+ * <ja>@RestOptions</ja>(defaultRequestAttributes={<js>"Foo: bar"</js>})
+ * <jk>public</jk> Object myMethod() {...}
+ * }
+ * </p>
+ *
+ * </ul>
+ * <ul class='notes'>
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestContext.Builder#defaultRequestAttributes(NamedAttribute...)}
+ * <li class='ja'>{@link Rest#defaultRequestAttributes()}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] defaultRequestAttributes() default {};
+
+ /**
+ * Default request headers.
+ *
+ * <p>
+ * Specifies default values for request headers if they're not passed in through the request.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Assume "text/json" Accept value when Accept not specified</jc>
+ * <ja>@RestOptions</ja>(path=<js>"/*"</js>, defaultRequestHeaders={<js>"Accept: text/json"</js>})
+ * <jk>public</jk> String doGet() {...}
+ * </p>
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$S{mySystemProperty}"</js>).
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestContext.Builder#defaultRequestHeaders(org.apache.http.Header...)}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] defaultRequestHeaders() default {};
+
+ /**
+ * Default response headers.
+ *
+ * <p>
+ * Specifies default values for response headers if they're not overwritten during the request.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Assume "text/json" Accept value when Accept not specified</jc>
+ * <ja>@RestOptions</ja>(path=<js>"/*"</js>, defaultResponseHeaders={<js>"Content-Type: text/json"</js>})
+ * <jk>public</jk> String doGet() {...}
+ * </p>
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$S{mySystemProperty}"</js>).
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestContext.Builder#defaultResponseHeaders(org.apache.http.Header...)}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] defaultResponseHeaders() default {};
+
+ /**
+ * Optional description for the exposed API.
+ *
+ * <p>
+ * This description is used in the following locations:
+ * <ul class='spaced-list'>
+ * <li>
+ * The value returned by {@link Operation#getDescription()} in the auto-generated swagger.
+ * <li>
+ * The <js>"$RS{operationDescription}"</js> variable.
+ * <li>
+ * The description of the method in the Swagger page.
+ * </ul>
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * Corresponds to the swagger field <c>/paths/{path}/{method}/description</c>.
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] description() default {};
+
+ /**
+ * Specifies the compression encoders for this method.
+ *
+ * <p>
+ * Encoders are used to enable various kinds of compression (e.g. <js>"gzip"</js>) on requests and responses.
+ *
+ * <p>
+ * This value overrides encoders specified at the class level using {@link Rest#encoders()}.
+ * The {@link org.apache.juneau.encoders.EncoderSet.Inherit} class can be used to include values from the parent class.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Define a REST resource that handles GZIP compression.</jc>
+ * <ja>@Rest</ja>(
+ * encoders={
+ * GzipEncoder.<jk>class</jk>
+ * }
+ * )
+ * <jk>public class</jk> MyResource {
+ *
+ * <jc>// Define a REST method that can also use a custom encoder.</jc>
+ * <ja>@RestOptions</ja>(
+ * method=<jsf>GET</jsf>,
+ * encoders={
+ * EncoderSet.Inherit.<jk>class</jk>, MyEncoder.<jk>class</jk>
+ * }
+ * )
+ * <jk>public</jk> MyBean doGet() {
+ * ...
+ * }
+ * }
+ * </p>
+ *
+ * <p>
+ * The programmatic equivalent to this annotation is:
+ * <p class='bjava'>
+ * RestOpContext.Builder <jv>builder</jv> = RestOpContext.<jsm>create</jsm>(<jv>method</jv>,<jv>restContext</jv>);
+ * <jv>builder</jv>.getEncoders().set(<jv>classes</jv>);
+ * </p>
+ *
+ * <ul class='seealso'>
+ * <li class='link'>{@doc jrs.Encoders}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ Class<? extends Encoder>[] encoders() default {};
+
+ /**
+ * Method-level guards.
+ *
+ * <p>
+ * Associates one or more {@link RestGuard RestGuards} with this method.
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestOpContext.Builder#guards()}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ Class<? extends RestGuard>[] guards() default {};
+
+ /**
+ * Method matchers.
+ *
+ * <p>
+ * Associates one more more {@link RestMatcher RestMatchers} with this method.
+ *
+ * <p>
+ * Matchers are used to allow multiple Java methods to handle requests assigned to the same URL path pattern, but
+ * differing based on some request attribute, such as a specific header value.
+ *
+ * <ul class='seealso'>
+ * <li class='jac'>{@link RestMatcher}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ Class<? extends RestMatcher>[] matchers() default {};
+
+ /**
+ * Dynamically apply this annotation to the specified methods.
+ *
+ * <ul class='seealso'>
+ * <li class='link'>{@doc jm.DynamicallyAppliedAnnotations}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] on() default {};
+
+ /**
+ * Optional path pattern for the specified method.
+ *
+ * <p>
+ * Appending <js>"/*"</js> to the end of the path pattern will make it match any remainder too.
+ * <br>Not appending <js>"/*"</js> to the end of the pattern will cause a 404 (Not found) error to occur if the exact
+ * pattern is not found.
+ *
+ * <p>
+ * The path can contain variables that get resolved to {@link org.apache.juneau.http.annotation.Path @Path} parameters.
+ *
+ * <h5 class='figure'>Examples:</h5>
+ * <p class='bjava'>
+ * <ja>@RestOptions</ja>(path=<js>"/myurl/{foo}/{bar}/{baz}/*"</js>)
+ * </p>
+ * <p class='bjava'>
+ * <ja>@RestOptions</ja>(path=<js>"/myurl/{0}/{1}/{2}/*"</js>)
+ * </p>
+ *
+ * <p>
+ * Note that you can also use {@link #value()} to specify the path.
+ *
+ * <ul class='seealso'>
+ * <li class='ja'>{@link org.apache.juneau.http.annotation.Path}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] path() default {};
+
+ /**
+ * Supported accept media types.
+ *
+ * <p>
+ * Overrides the media types inferred from the serializers that identify what media types can be produced by the resource.
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$S{mySystemProperty}"</js>).
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestOpContext.Builder#produces(MediaType...)}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] produces() default {};
+
+ /**
+ * Role guard.
+ *
+ * <p>
+ * An expression defining if a user with the specified roles are allowed to access this method.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jk>public class</jk> MyResource <jk>extends</jk> BasicRestServlet <jk>implements</jk> BasicUniversalConfig {
+ *
+ * <ja>@RestOptions</ja>(
+ * path=<js>"/foo"</js>,
+ * roleGuard=<js>"ROLE_ADMIN || (ROLE_READ_WRITE && ROLE_SPECIAL)"</js>
+ * )
+ * <jk>public</jk> Object doGet() {
+ * }
+ * }
+ * </p>
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * Supports any of the following expression constructs:
+ * <ul>
+ * <li><js>"foo"</js> - Single arguments.
+ * <li><js>"foo,bar,baz"</js> - Multiple OR'ed arguments.
+ * <li><js>"foo | bar | baz"</js> - Multiple OR'ed arguments, pipe syntax.
+ * <li><js>"foo || bar || baz"</js> - Multiple OR'ed arguments, Java-OR syntax.
+ * <li><js>"fo*"</js> - Patterns including <js>'*'</js> and <js>'?'</js>.
+ * <li><js>"fo* & *oo"</js> - Multiple AND'ed arguments, ampersand syntax.
+ * <li><js>"fo* && *oo"</js> - Multiple AND'ed arguments, Java-AND syntax.
+ * <li><js>"fo* || (*oo || bar)"</js> - Parenthesis.
+ * </ul>
+ * <li class='note'>
+ * AND operations take precedence over OR operations (as expected).
+ * <li class='note'>
+ * Whitespace is ignored.
+ * <li class='note'>
+ * <jk>null</jk> or empty expressions always match as <jk>false</jk>.
+ * <li class='note'>
+ * If patterns are used, you must specify the list of declared roles using {@link #rolesDeclared()} or {@link org.apache.juneau.rest.RestOpContext.Builder#rolesDeclared(String...)}.
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * <li class='note'>
+ * When defined on parent/child classes and methods, ALL guards within the hierarchy must pass.
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestOpContext.Builder#roleGuard(String)}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String roleGuard() default "";
+
+ /**
+ * Declared roles.
+ *
+ * <p>
+ * A comma-delimited list of all possible user roles.
+ *
+ * <p>
+ * Used in conjunction with {@link #roleGuard()} is used with patterns.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jk>public class</jk> MyResource <jk>extends</jk> BasicRestServlet <jk>implements</jk> BasicUniversalConfig {
+ *
+ * <ja>@RestOptions</ja>(
+ * path=<js>"/foo"</js>,
+ * rolesDeclared=<js>"ROLE_ADMIN,ROLE_READ_WRITE,ROLE_READ_ONLY,ROLE_SPECIAL"</js>,
+ * roleGuard=<js>"ROLE_ADMIN || (ROLE_READ_WRITE && ROLE_SPECIAL)"</js>
+ * )
+ * <jk>public</jk> Object doGet() {
+ * }
+ * }
+ * </p>
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.rest.RestOpContext.Builder#rolesDeclared(String...)}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String rolesDeclared() default "";
+
+ /**
+ * Specifies the serializers for marshalling POJOs into response bodies for this method.
+ *
+ * <p>
+ * Serializer are used to convert POJOs to HTTP response bodies.
+ * <br>Any of the Juneau framework serializers can be used in this setting.
+ * <br>The serializer selected is based on the request <c>Accept</c> header matched against the values returned by the following method
+ * using a best-match algorithm:
+ * <ul class='javatree'>
+ * <li class='jm'>{@link Serializer#getMediaTypeRanges()}
+ * </ul>
+ *
+ * <p>
+ * This value overrides serializers specified at the class level using {@link Rest#serializers()}.
+ * The {@link org.apache.juneau.serializer.SerializerSet.Inherit} class can be used to include values from the parent class.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Define a REST resource that can produce JSON and HTML.</jc>
+ * <ja>@Rest</ja>(
+ * serializers={
+ * JsonParser.<jk>class</jk>,
+ * HtmlParser.<jk>class</jk>
+ * }
+ * )
+ * <jk>public class</jk> MyResource {
+ *
+ * <jc>// Define a REST method that can also produce XML.</jc>
+ * <ja>@RestOptions</ja>(
+ * parsers={
+ * SerializerSet.Inherit.<jk>class</jk>, XmlParser.<jk>class</jk>
+ * }
+ * )
+ * <jk>public</jk> MyBean doGet() {
+ * ...
+ * }
+ * }
+ * </p>
+ *
+ * <p>
+ * The programmatic equivalent to this annotation is:
+ * <p class='bjava'>
+ * RestOpContext.Builder <jv>builder</jv> = RestOpContext.<jsm>create</jsm>(<jv>method</jv>,<jv>restContext</jv>);
+ * <jv>builder</jv>.getSerializers().set(<jv>classes</jv>);
+ * </p>
+ *
+ * <ul class='seealso'>
+ * <li class='link'>{@doc jrs.Marshalling}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ Class<? extends Serializer>[] serializers() default {};
+
+ /**
+ * Optional summary for the exposed API.
+ *
+ * <p>
+ * This summary is used in the following locations:
+ * <ul class='spaced-list'>
+ * <li>
+ * The value returned by {@link Operation#getSummary()} in the auto-generated swagger.
+ * <li>
+ * The <js>"$RS{operationSummary}"</js> variable.
+ * <li>
+ * The summary of the method in the Swagger page.
+ * </ul>
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * Corresponds to the swagger field <c>/paths/{path}/{method}/summary</c>.
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String summary() default "";
+
+ /**
+ * Provides swagger-specific metadata on this method.
+ *
+ * <p>
+ * Used to populate the auto-generated OPTIONS swagger documentation.
+ *
+ * <p>
+ * The format of this annotation is JSON when all individual parts are concatenated.
+ * <br>The starting and ending <js>'{'</js>/<js>'}'</js> characters around the entire value are optional.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <ja>@RestOptions</ja>(
+ * path=<js>"/{propertyName}"</js>,
+ *
+ * <jc>// Swagger info.</jc>
+ * swagger={
+ * <js>"parameters:["</js>,
+ * <js>"{name:'propertyName',in:'path',description:'The system property name.'},"</js>,
+ * <js>"{in:'body',description:'The new system property value.'}"</js>,
+ * <js>"],"</js>,
+ * <js>"responses:{"</js>,
+ * <js>"302: {headers:{Location:{description:'The root URL of this resource.'}}},"</js>,
+ * <js>"403: {description:'User is not an admin.'}"</js>,
+ * <js>"}"</js>
+ * }
+ * )
+ * </p>
+ *
+ * <ul class='notes'>
+ * <li class='note'>
+ * The format is {@doc jd.Swagger}.
+ * <br>Multiple lines are concatenated with newlines.
+ * <li class='note'>
+ * The starting and ending <js>'{'</js>/<js>'}'</js> characters around the entire value are optional.
+ * <li class='note'>
+ * These values are superimposed on top of any Swagger JSON file present for the resource in the classpath.
+ * <li class='note'>
+ * Supports {@doc jrs.SvlVariables}
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * </ul>
+ *
+ * <ul class='seealso'>
+ * <li class='ja'>{@link OpSwagger}
+ * <li class='jc'>{@link SwaggerProvider}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ OpSwagger swagger() default @OpSwagger;
+
+ /**
+ * REST method path.
+ *
+ * <p>
+ * Can be used to provide a shortened form for the {@link #path()} value.
+ *
+ * <p>
+ * The following examples are considered equivalent.
+ * <p class='bjava'>
+ * <jc>// Normal form</jc>
+ * <ja>@RestOptions</ja>(path=<js>"/{propertyName}"</js>)
+ *
+ * <jc>// Shortened form</jc>
+ * <ja>@RestOptions</ja>(<js>"/{propertyName}"</js>)
+ * </p>
+ *
+ * @return The annotation value.
+ */
+ String value() default "";
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestOptionsAnnotation.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestOptionsAnnotation.java
new file mode 100644
index 000000000..4cbfacf0f
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestOptionsAnnotation.java
@@ -0,0 +1,538 @@
+// ***************************************************************************************************************************
+// * 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.annotation;
+
+import static org.apache.juneau.http.HttpHeaders.*;
+import static org.apache.juneau.internal.ArrayUtils.*;
+import static org.apache.juneau.http.HttpParts.*;
+
+import java.lang.annotation.*;
+import java.nio.charset.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.encoders.*;
+import org.apache.juneau.reflect.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.converter.*;
+import org.apache.juneau.rest.guard.*;
+import org.apache.juneau.rest.httppart.*;
+import org.apache.juneau.rest.matcher.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Utility classes and methods for the {@link RestOptions @RestOptions} annotation.
+ *
+ * <ul class='seealso'>
+ * <li class='link'>{@doc jrs.RestOpAnnotatedMethods}
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+public class RestOptionsAnnotation {
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Static
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /** Default value */
+ public static final RestOptions DEFAULT = create().build();
+
+ /**
+ * Instantiates a new builder for this class.
+ *
+ * @return A new builder object.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Builder
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Builder class.
+ *
+ * <ul class='seealso'>
+ * <li class='jm'>{@link org.apache.juneau.BeanContext.Builder#annotations(Annotation...)}
+ * </ul>
+ */
+ @SuppressWarnings("unchecked")
+ public static class Builder extends TargetedAnnotationMBuilder {
+
+ Class<? extends RestConverter>[] converters = new Class[0];
+ Class<? extends RestGuard>[] guards = new Class[0];
+ Class<? extends RestMatcher>[] matchers = new Class[0];
+ Class<? extends Encoder>[] encoders = new Class[0];
+ Class<? extends Serializer>[] serializers = new Class[0];
+ OpSwagger swagger = OpSwaggerAnnotation.DEFAULT;
+ String clientVersion="", debug="", defaultAccept="", defaultCharset="", rolesDeclared="", roleGuard="", summary="", value="";
+ String[] defaultRequestQueryData={}, defaultRequestAttributes={}, defaultRequestHeaders={}, defaultResponseHeaders={}, description={}, path={}, produces={};
+
+ /**
+ * Constructor.
+ */
+ protected Builder() {
+ super(RestOptions.class);
+ }
+
+ /**
+ * Instantiates a new {@link RestOptions @RestOptions} object initialized with this builder.
+ *
+ * @return A new {@link RestOptions @RestOptions} object.
+ */
+ public RestOptions build() {
+ return new Impl(this);
+ }
+
+ /**
+ * Sets the {@link RestOptions#clientVersion()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder clientVersion(String value) {
+ this.clientVersion = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#converters()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder converters(Class<? extends RestConverter>...value) {
+ this.converters = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#debug()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder debug(String value) {
+ this.debug = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#defaultAccept()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder defaultAccept(String value) {
+ this.defaultAccept = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#defaultCharset()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder defaultCharset(String value) {
+ this.defaultCharset = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#defaultRequestQueryData()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder defaultRequestQueryData(String...value) {
+ this.defaultRequestQueryData = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#defaultRequestAttributes()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder defaultRequestAttributes(String...value) {
+ this.defaultRequestAttributes = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#defaultRequestHeaders()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder defaultRequestHeaders(String...value) {
+ this.defaultRequestHeaders = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#defaultResponseHeaders()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder defaultResponseHeaders(String...value) {
+ this.defaultResponseHeaders = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#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 RestOptions#encoders()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder encoders(Class<? extends Encoder>...value) {
+ this.encoders = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#guards()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder guards(Class<? extends RestGuard>...value) {
+ this.guards = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#matchers()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder matchers(Class<? extends RestMatcher>...value) {
+ this.matchers = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#path()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder path(String...value) {
+ this.path = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#produces()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder produces(String...value) {
+ this.produces = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#roleGuard()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder roleGuard(String value) {
+ this.roleGuard = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#rolesDeclared()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder rolesDeclared(String value) {
+ this.rolesDeclared = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#serializers()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder serializers(Class<? extends Serializer>...value) {
+ this.serializers = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#summary()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder summary(String value) {
+ this.summary = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#swagger()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder swagger(OpSwagger value) {
+ this.swagger = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link RestOptions#value()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder value(String value) {
+ this.value = value;
+ return this;
+ }
+
+ // <FluentSetters>
+
+ @Override /* GENERATED - TargetedAnnotationBuilder */
+ public Builder on(String...values) {
+ super.on(values);
+ return this;
+ }
+
+ @Override /* GENERATED - TargetedAnnotationTMBuilder */
+ public Builder on(java.lang.reflect.Method...value) {
+ super.on(value);
+ return this;
+ }
+
+ // </FluentSetters>
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Implementation
+ //-----------------------------------------------------------------------------------------------------------------
+
+ private static class Impl extends TargetedAnnotationImpl implements RestOptions {
+
+ private final Class<? extends RestConverter>[] converters;
+ private final Class<? extends RestGuard>[] guards;
+ private final Class<? extends RestMatcher>[] matchers;
+ private final Class<? extends Encoder>[] encoders;
+ private final Class<? extends Serializer>[] serializers;
+ private final OpSwagger swagger;
+ private final String clientVersion, debug, defaultAccept, defaultCharset, rolesDeclared, roleGuard, summary, value;
+ private final String[] defaultRequestQueryData, defaultRequestAttributes, defaultRequestHeaders, defaultResponseHeaders, description, path, produces;
+
+ Impl(Builder b) {
+ super(b);
+ this.clientVersion = b.clientVersion;
+ this.converters = copyOf(b.converters);
+ this.debug = b.debug;
+ this.defaultAccept = b.defaultAccept;
+ this.defaultCharset = b.defaultCharset;
+ this.defaultRequestQueryData = copyOf(b.defaultRequestQueryData);
+ this.defaultRequestAttributes = copyOf(b.defaultRequestAttributes);
+ this.defaultRequestHeaders = copyOf(b.defaultRequestHeaders);
+ this.defaultResponseHeaders = copyOf(b.defaultResponseHeaders);
+ this.description = copyOf(b.description);
+ this.encoders = copyOf(b.encoders);
+ this.guards = copyOf(b.guards);
+ this.matchers = copyOf(b.matchers);
+ this.path = copyOf(b.path);
+ this.produces = copyOf(b.produces);
+ this.roleGuard = b.roleGuard;
+ this.rolesDeclared = b.rolesDeclared;
+ this.serializers = copyOf(b.serializers);
+ this.summary = b.summary;
+ this.swagger = b.swagger;
+ this.value = b.value;
+ postConstruct();
+ }
+
+ @Override /* RestOptions */
+ public String clientVersion() {
+ return clientVersion;
+ }
+
+ @Override /* RestOptions */
+ public Class<? extends RestConverter>[] converters() {
+ return converters;
+ }
+
+ @Override /* RestOptions */
+ public String debug() {
+ return debug;
+ }
+
+ @Override /* RestOptions */
+ public String defaultAccept() {
+ return defaultAccept;
+ }
+
+ @Override /* RestOptions */
+ public String defaultCharset() {
+ return defaultCharset;
+ }
+
+ @Override /* RestOptions */
+ public String[] defaultRequestQueryData() {
+ return defaultRequestQueryData;
+ }
+
+ @Override /* RestOptions */
+ public String[] defaultRequestAttributes() {
+ return defaultRequestAttributes;
+ }
+
+ @Override /* RestOptions */
+ public String[] defaultRequestHeaders() {
+ return defaultRequestHeaders;
+ }
+
+ @Override /* RestOptions */
+ public String[] defaultResponseHeaders() {
+ return defaultResponseHeaders;
+ }
+
+ @Override /* RestOptions */
+ public String[] description() {
+ return description;
+ }
+
+ @Override /* RestOptions */
+ public Class<? extends Encoder>[] encoders() {
+ return encoders;
+ }
+
+ @Override /* RestOptions */
+ public Class<? extends RestGuard>[] guards() {
+ return guards;
+ }
+
+ @Override /* RestOptions */
+ public Class<? extends RestMatcher>[] matchers() {
+ return matchers;
+ }
+
+ @Override /* RestOptions */
+ public String[] path() {
+ return path;
+ }
+
+ @Override /* RestOptions */
+ public String[] produces() {
+ return produces;
+ }
+
+ @Override /* RestOptions */
+ public String roleGuard() {
+ return roleGuard;
+ }
+
+ @Override /* RestOptions */
+ public String rolesDeclared() {
+ return rolesDeclared;
+ }
+
+ @Override /* RestOptions */
+ public Class<? extends Serializer>[] serializers() {
+ return serializers;
+ }
+
+ @Override /* RestOptions */
+ public String summary() {
+ return summary;
+ }
+
+ @Override /* RestOptions */
+ public OpSwagger swagger() {
+ return swagger;
+ }
+
+ @Override /* RestOptions */
+ public String value() {
+ return value;
+ }
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Appliers
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Applies {@link RestOptions} annotations to a {@link org.apache.juneau.rest.RestOpContext.Builder}.
+ */
+ public static class RestOpContextApply extends AnnotationApplier<RestOptions,RestOpContext.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public RestOpContextApply(VarResolverSession vr) {
+ super(RestOptions.class, RestOpContext.Builder.class, vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<RestOptions> ai, RestOpContext.Builder b) {
+ RestOptions a = ai.inner();
+
+ b.httpMethod("options");
+
+ classes(a.serializers()).ifPresent(x -> b.serializers().set(x));
+ classes(a.encoders()).ifPresent(x -> b.encoders().set(x));
+ stream(a.produces()).map(MediaType::of).forEach(x -> b.produces(x));
+ stream(a.defaultRequestHeaders()).map(x -> stringHeader(x)).forEach(x -> b.defaultRequestHeaders().setDefault(x));
+ stream(a.defaultResponseHeaders()).map(x -> stringHeader(x)).forEach(x -> b.defaultResponseHeaders().setDefault(x));
+ stream(a.defaultRequestAttributes()).map(x -> BasicNamedAttribute.ofPair(x)).forEach(x -> b.defaultRequestAttributes().add(x));
+ stream(a.defaultRequestQueryData()).map(x -> basicPart(x)).forEach(x -> b.defaultRequestQueryData().setDefault(x));
+ string(a.defaultAccept()).map(x -> accept(x)).ifPresent(x -> b.defaultRequestHeaders().setDefault(x));
+ b.converters().append(a.converters());
+ b.guards().append(a.guards());
+ b.matchers().append(a.matchers());
+ string(a.clientVersion()).ifPresent(x -> b.clientVersion(x));
+ string(a.defaultCharset()).map(Charset::forName).ifPresent(x -> b.defaultCharset(x));
+ stream(a.path()).forEach(x -> b.path(x));
+ string(a.value()).ifPresent(x -> b.path(x));
+ cdl(a.rolesDeclared()).forEach(x -> b.rolesDeclared(x));
+ string(a.roleGuard()).ifPresent(x -> b.roleGuard(x));
+ string(a.debug()).map(Enablement::fromString).ifPresent(x -> b.debug(x));
+ }
+ }
+}
\ 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 35b09b75e..66bba29cc 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
@@ -79,64 +79,64 @@ public class Remote_FormDataAnnotation_Test {
@Remote
public static interface A1 {
- @RemoteOp(path="a") String postX1(@FormData("x") int b);
- @RemoteOp(path="a") String postX2(@FormData("x") float b);
- @RemoteOp(path="a") String postX3(@FormData("x") Bean b);
- @RemoteOp(path="a") String postX4(@FormData("*") Bean b);
- @RemoteOp(path="a") String postX5(@FormData Bean b);
- @RemoteOp(path="a") String postX6(@FormData("x") Bean[] b);
- @RemoteOp(path="a") String postX7(@FormData("x") @Schema(cf="uon") Bean[] b);
- @RemoteOp(path="a") String postX8(@FormData("x") List<Bean> b);
- @RemoteOp(path="a") String postX9(@FormData("x") @Schema(cf="uon") List<Bean> b);
- @RemoteOp(path="a") String postX10(@FormData("x") Map<String,Bean> b);
- @RemoteOp(path="a") String postX11(@FormData("*") Map<String,Bean> b);
- @RemoteOp(path="a") String postX12(@FormData Map<String,Bean> b);
- @RemoteOp(path="a") String postX13(@FormData("x") @Schema(f="uon") Map<String,Bean> b);
- @RemoteOp(path="a") String postX14(@FormData @Schema(f="uon") Map<String,Bean> b);
- @RemoteOp(path="a") String postX15(@FormData("*") Reader b);
- @RemoteOp(path="a") String postX16(@FormData Reader b);
- @RemoteOp(path="a") String postX17(@FormData("*") InputStream b);
- @RemoteOp(path="a") String postX18(@FormData InputStream b);
- @RemoteOp(path="a") String postX19(@FormData("*") PartList b);
- @RemoteOp(path="a") String postX20(@FormData PartList b);
- @RemoteOp(path="a") String postX21(@FormData NameValuePair b);
- @RemoteOp(path="a") String postX22(@FormData String b);
- @RemoteOp(path="a") String postX23(@FormData InputStream b);
- @RemoteOp(path="a") String postX24(@FormData Reader b);
- @RemoteOp(path="a") String postX25(@FormData Bean2 b);
- @RemoteOp(path="a") String postX26(@FormData List<NameValuePair> b);
+ @RemotePost(path="a") String x1(@FormData("x") int b);
+ @RemotePost(path="a") String x2(@FormData("x") float b);
+ @RemotePost(path="a") String x3(@FormData("x") Bean b);
+ @RemotePost(path="a") String x4(@FormData("*") Bean b);
+ @RemotePost(path="a") String x5(@FormData Bean b);
+ @RemotePost(path="a") String x6(@FormData("x") Bean[] b);
+ @RemotePost(path="a") String x7(@FormData("x") @Schema(cf="uon") Bean[] b);
+ @RemotePost(path="a") String x8(@FormData("x") List<Bean> b);
+ @RemotePost(path="a") String x9(@FormData("x") @Schema(cf="uon") List<Bean> b);
+ @RemotePost(path="a") String x10(@FormData("x") Map<String,Bean> b);
+ @RemotePost(path="a") String x11(@FormData("*") Map<String,Bean> b);
+ @RemotePost(path="a") String x12(@FormData Map<String,Bean> b);
+ @RemotePost(path="a") String x13(@FormData("x") @Schema(f="uon") Map<String,Bean> b);
+ @RemotePost(path="a") String x14(@FormData @Schema(f="uon") Map<String,Bean> b);
+ @RemotePost(path="a") String x15(@FormData("*") Reader b);
+ @RemotePost(path="a") String x16(@FormData Reader b);
+ @RemotePost(path="a") String x17(@FormData("*") InputStream b);
+ @RemotePost(path="a") String x18(@FormData InputStream b);
+ @RemotePost(path="a") String x19(@FormData("*") PartList b);
+ @RemotePost(path="a") String x20(@FormData PartList b);
+ @RemotePost(path="a") String x21(@FormData NameValuePair b);
+ @RemotePost(path="a") String x22(@FormData String b);
+ @RemotePost(path="a") String x23(@FormData InputStream b);
+ @RemotePost(path="a") String x24(@FormData Reader b);
+ @RemotePost(path="a") String x25(@FormData Bean2 b);
+ @RemotePost(path="a") String x26(@FormData List<NameValuePair> b);
}
@Test
public void a01_objectTypes() throws Exception {
A1 x = MockRestClient.build(A.class).getRemote(A1.class);
- assertEquals("{x:'1'}",x.postX1(1));
- assertEquals("{x:'1.0'}",x.postX2(1));
- assertEquals("{x:'f=1'}",x.postX3(Bean.create()));
- assertEquals("{f:'1'}",x.postX4(Bean.create()));
- assertEquals("{f:'1'}",x.postX5(Bean.create()));
- assertEquals("{x:'f=1,f=1'}",x.postX6(new Bean[]{Bean.create(),Bean.create()}));
- assertEquals("{x:'@((f=1),(f=1))'}",x.postX7(new Bean[]{Bean.create(),Bean.create()}));
- assertEquals("{x:'f=1,f=1'}",x.postX8(alist(Bean.create(),Bean.create())));
- assertEquals("{x:'@((f=1),(f=1))'}",x.postX9(alist(Bean.create(),Bean.create())));
- assertEquals("{x:'k1=f\\\\=1'}",x.postX10(map("k1",Bean.create())));
- assertEquals("{k1:'f=1'}",x.postX11(map("k1",Bean.create())));
- assertEquals("{k1:'f=1'}",x.postX12(map("k1",Bean.create())));
- assertEquals("{x:'k1=f\\\\=1'}",x.postX13(map("k1",Bean.create())));
- assertEquals("{k1:'f=1'}",x.postX14(map("k1",Bean.create())));
- assertEquals("{x:'1'}",x.postX15(reader("x=1")));
- assertEquals("{x:'1'}",x.postX16(reader("x=1")));
- assertEquals("{x:'1'}",x.postX17(inputStream("x=1")));
- assertEquals("{x:'1'}",x.postX18(inputStream("x=1")));
- assertEquals("{foo:'bar'}",x.postX19(parts("foo","bar")));
- assertEquals("{foo:'bar'}",x.postX20(parts("foo","bar")));
- assertEquals("{foo:'bar'}",x.postX21(part("foo","bar")));
- assertEquals("{foo:'bar'}",x.postX22("foo=bar"));
- assertEquals("{}",x.postX22(null));
- assertEquals("{foo:'bar'}",x.postX23(inputStream("foo=bar")));
- assertEquals("{foo:'bar'}",x.postX24(reader("foo=bar")));
- assertEquals("{f:'1'}",x.postX25(Bean2.create()));
- assertEquals("{foo:'bar'}",x.postX26(alist(part("foo","bar"))));
+ assertEquals("{x:'1'}",x.x1(1));
+ assertEquals("{x:'1.0'}",x.x2(1));
+ assertEquals("{x:'f=1'}",x.x3(Bean.create()));
+ assertEquals("{f:'1'}",x.x4(Bean.create()));
+ assertEquals("{f:'1'}",x.x5(Bean.create()));
+ assertEquals("{x:'f=1,f=1'}",x.x6(new Bean[]{Bean.create(),Bean.create()}));
+ assertEquals("{x:'@((f=1),(f=1))'}",x.x7(new Bean[]{Bean.create(),Bean.create()}));
+ assertEquals("{x:'f=1,f=1'}",x.x8(alist(Bean.create(),Bean.create())));
+ assertEquals("{x:'@((f=1),(f=1))'}",x.x9(alist(Bean.create(),Bean.create())));
+ assertEquals("{x:'k1=f\\\\=1'}",x.x10(map("k1",Bean.create())));
+ assertEquals("{k1:'f=1'}",x.x11(map("k1",Bean.create())));
+ assertEquals("{k1:'f=1'}",x.x12(map("k1",Bean.create())));
+ assertEquals("{x:'k1=f\\\\=1'}",x.x13(map("k1",Bean.create())));
+ assertEquals("{k1:'f=1'}",x.x14(map("k1",Bean.create())));
+ assertEquals("{x:'1'}",x.x15(reader("x=1")));
+ assertEquals("{x:'1'}",x.x16(reader("x=1")));
+ assertEquals("{x:'1'}",x.x17(inputStream("x=1")));
+ assertEquals("{x:'1'}",x.x18(inputStream("x=1")));
+ assertEquals("{foo:'bar'}",x.x19(parts("foo","bar")));
+ assertEquals("{foo:'bar'}",x.x20(parts("foo","bar")));
+ assertEquals("{foo:'bar'}",x.x21(part("foo","bar")));
+ assertEquals("{foo:'bar'}",x.x22("foo=bar"));
+ assertEquals("{}",x.x22(null));
+ assertEquals("{foo:'bar'}",x.x23(inputStream("foo=bar")));
+ assertEquals("{foo:'bar'}",x.x24(reader("foo=bar")));
+ assertEquals("{f:'1'}",x.x25(Bean2.create()));
+ assertEquals("{foo:'bar'}",x.x26(alist(part("foo","bar"))));
}
//-----------------------------------------------------------------------------------------------------------------
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_Test.java
index 7c8675711..2f2655e8d 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_Test.java
@@ -27,6 +27,7 @@ import org.apache.juneau.rest.config.*;
import org.apache.juneau.rest.mock.*;
import org.apache.juneau.rest.servlet.*;
import org.apache.juneau.http.*;
+import org.apache.juneau.http.annotation.*;
import org.apache.juneau.http.header.*;
import org.apache.juneau.marshaller.*;
import org.junit.*;
@@ -622,6 +623,108 @@ public class Remote_Test {
assertThrown(()->client(G.class).header("Check","Foo").build().getRemote(G1.class)).isType(RemoteMetadataException.class).asMessage().isContains("Invalid value");
}
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Method detection
+ //-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class H extends BasicRestObject {
+ @RestOp(method="*", path="/*")
+ public String echoMethod(@Method String method, @Path("/*") String path) {
+ return method + " " + path;
+ }
+ }
+
+ @Remote
+ public static interface H1 {
+ @RemoteOp(method="get") String a1();
+ @RemoteOp(method="put") String a2();
+ @RemoteOp(method="post") String a3();
+ @RemoteOp(method="patch") String a4();
+ @RemoteOp(method="delete") String a5();
+ @RemoteOp(method="options") String a6();
+ @RemoteGet String a11();
+ @RemotePut String a12();
+ @RemotePost String a13();
+ @RemotePatch String a14();
+ @RemoteDelete String a15();
+ @RemoteOp String getA21();
+ @RemoteOp String putA22();
+ @RemoteOp String postA23();
+ @RemoteOp String patchA24();
+ @RemoteOp String deleteA25();
+ @RemoteOp String optionsA26();
+ @RemoteGet("/a31x") String a31();
+ @RemotePut("/a32x") String a32();
+ @RemotePost("/a33x") String a33();
+ @RemotePatch("/a34x") String a34();
+ @RemoteDelete("/a35x") String a35();
+ @RemoteOp("GET /a41x") String a41();
+ @RemoteOp("PUT /a42x") String a42();
+ @RemoteOp("POST /a43x") String a43();
+ @RemoteOp("PATCH /a44x") String a44();
+ @RemoteOp("DELETE /a45x") String a45();
+ @RemoteOp("OPTIONS /a46x") String a46();
+ @RemoteGet("a51x") String a51();
+ @RemotePut("a52x") String a52();
+ @RemotePost("a53x") String a53();
+ @RemotePatch("a54x") String a54();
+ @RemoteDelete("a55x") String a55();
+ @RemoteOp("GET a61x") String a61();
+ @RemoteOp("PUT a62x") String a62();
+ @RemoteOp("POST a63x") String a63();
+ @RemoteOp("PATCH a64x") String a64();
+ @RemoteOp("DELETE a65x") String a65();
+ @RemoteOp("OPTIONS a66x") String a66();
+ }
+
+
+ @Test
+ public void h01_methodDetection() throws Exception {
+
+ H1 x = client(H.class).build().getRemote(H1.class);
+ assertEquals("GET a1", x.a1());
+ assertEquals("PUT a2", x.a2());
+ assertEquals("POST a3", x.a3());
+ assertEquals("PATCH a4", x.a4());
+ assertEquals("DELETE a5", x.a5());
+ assertEquals("OPTIONS a6", x.a6());
+ assertEquals("GET a11", x.a11());
+ assertEquals("PUT a12", x.a12());
+ assertEquals("POST a13", x.a13());
+ assertEquals("PATCH a14", x.a14());
+ assertEquals("DELETE a15", x.a15());
+ assertEquals("GET a21", x.getA21());
+ assertEquals("PUT a22", x.putA22());
+ assertEquals("POST a23", x.postA23());
+ assertEquals("PATCH a24", x.patchA24());
+ assertEquals("DELETE a25", x.deleteA25());
+ assertEquals("OPTIONS a26", x.optionsA26());
+ assertEquals("GET a31x", x.a31());
+ assertEquals("PUT a32x", x.a32());
+ assertEquals("POST a33x", x.a33());
+ assertEquals("PATCH a34x", x.a34());
+ assertEquals("DELETE a35x", x.a35());
+ assertEquals("GET a41x", x.a41());
+ assertEquals("PUT a42x", x.a42());
+ assertEquals("POST a43x", x.a43());
+ assertEquals("PATCH a44x", x.a44());
+ assertEquals("DELETE a45x", x.a45());
+ assertEquals("OPTIONS a46x", x.a46());
+ assertEquals("GET a51x", x.a51());
+ assertEquals("PUT a52x", x.a52());
+ assertEquals("POST a53x", x.a53());
+ assertEquals("PATCH a54x", x.a54());
+ assertEquals("DELETE a55x", x.a55());
+ assertEquals("GET a61x", x.a61());
+ assertEquals("PUT a62x", x.a62());
+ assertEquals("POST a63x", x.a63());
+ assertEquals("PATCH a64x", x.a64());
+ assertEquals("DELETE a65x", x.a65());
+ assertEquals("OPTIONS a66x", x.a66());
+ }
+
//-----------------------------------------------------------------------------------------------------------------
// Helper methods.
//-----------------------------------------------------------------------------------------------------------------