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>&lt;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 &amp;&amp; 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* &amp; *oo"</js> - Multiple AND'ed arguments, ampersand syntax.
+	 * 			<li><js>"fo* &amp;&amp; *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 &amp;&amp; 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.
 	//-----------------------------------------------------------------------------------------------------------------