REST refactoring.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
index e5fcb75..d86e6b1 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
@@ -931,7 +931,7 @@
 		return new DefaultFilteringOMap()
 			.a("Context", new DefaultFilteringOMap()
 				.a("identityCode", identityCode)
-				.a("propertyStore", propertyStore)
+				.a("propertyStore", System.identityHashCode(propertyStore))
 			);
 	}
 }
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestContext_ThreadLocals_Test.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestContext_ThreadLocals_Test.java
index 7d6bd0f..3e815ef 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestContext_ThreadLocals_Test.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestContext_ThreadLocals_Test.java
@@ -12,7 +12,7 @@
 // ***************************************************************************************************************************

 package org.apache.juneau.rest;

 

-import static org.junit.Assert.*;

+import static org.apache.juneau.assertions.Assertions.*;

 import static org.junit.runners.MethodSorters.*;

 

 import org.apache.juneau.rest.annotation.*;

@@ -36,8 +36,8 @@
 

 		@RestHook(HookEvent.END_CALL)

 		public void assertThreadsNotSet() {

-			assertNull(getRequest());

-			assertNull(getResponse());

+			assertThrown(()->getRequest()).contains("No active request on current thread.");

+			assertThrown(()->getResponse()).contains("No active request on current thread.");

 		}

 	}

 	static MockRestClient a = MockRestClient.build(A.class);

@@ -62,8 +62,8 @@
 	public static class B extends BasicRestServletGroup {

 		@RestHook(HookEvent.END_CALL)

 		public void assertThreadsNotSet2() {

-			assertNull(getRequest());

-			assertNull(getResponse());

+			assertThrown(()->getRequest()).contains("No active request on current thread.");

+			assertThrown(()->getResponse()).contains("No active request on current thread.");

 		}

 	}

 	static MockRestClient b = MockRestClient.build(B.class);

diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 5f0cef5..4863d25 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -17,7 +17,6 @@
 import static org.apache.juneau.internal.ObjectUtils.*;

 import static org.apache.juneau.internal.IOUtils.*;

 import static org.apache.juneau.internal.StringUtils.*;

-import static org.apache.juneau.rest.util.RestUtils.*;

 import static org.apache.juneau.rest.HttpRuntimeException.*;

 import static org.apache.juneau.Enablement.*;

 import static java.util.Collections.*;

@@ -64,7 +63,6 @@
 import org.apache.juneau.rest.logging.*;

 import org.apache.juneau.rest.params.*;

 import org.apache.juneau.http.exception.*;

-import org.apache.juneau.http.remote.*;

 import org.apache.juneau.rest.reshandlers.*;

 import org.apache.juneau.rest.util.*;

 import org.apache.juneau.rest.vars.*;

@@ -3184,8 +3182,7 @@
 	private final Messages msgs;

 	private final Config config;

 	private final VarResolver varResolver;

-	private final Map<String,List<RestMethodContext>> methodMap;

-	private final List<RestMethodContext> methods;

+	private final RestMethods restMethods;

 	private final Map<String,RestContext> childResources;

 	private final StackTraceStore stackTraceStore;

 	private final Logger logger;

@@ -3366,105 +3363,8 @@
 			preCallMethods = createPreCallMethods(r).stream().map(this::toRestMethodInvoker).toArray(RestMethodInvoker[]:: new);

 			postCallMethods = createPostCallMethods(r).stream().map(this::toRestMethodInvoker).toArray(RestMethodInvoker[]:: new);

 

-			//----------------------------------------------------------------------------------------------------

-			// Initialize the child resources.

-			// Done after initializing fields above since we pass this object to the child resources.

-			//----------------------------------------------------------------------------------------------------

-			List<String> methodsFound = new LinkedList<>();   // Temporary to help debug transient duplicate method issue.

-			MethodMapBuilder methodMapBuilder = new MethodMapBuilder();

+			restMethods = createRestMethods(r).build();

 

-			for (MethodInfo mi : rci.getPublicMethods()) {

-				RestMethod a = mi.getLastAnnotation(RestMethod.class);

-

-				// Also include methods on @Rest-annotated interfaces.

-				if (a == null) {

-					for (Method mi2 : mi.getMatching()) {

-						Class<?> ci2 = mi2.getDeclaringClass();

-						if (ci2.isInterface() && ci2.getAnnotation(Rest.class) != null) {

-							a = RestMethodAnnotation.DEFAULT;

-						}

-					}

-				}

-				if (a != null) {

-					methodsFound.add(mi.getSimpleName() + "," + emptyIfNull(a.method()) + "," + fixMethodPath(a.path().length > 0 ? a.path()[0] : ""));

-					try {

-						if (mi.isNotPublic())

-							throw new RestServletException("@RestMethod method {0}.{1} must be defined as public.", rci.inner().getName(), mi.getSimpleName());

-

-						RestMethodContextBuilder rmcb = new RestMethodContextBuilder(r, mi.inner(), this);

-						RestMethodContext sm = new RestMethodContext(rmcb);

-						String httpMethod = sm.getHttpMethod();

-

-						// RRPC is a special case where a method returns an interface that we

-						// can perform REST calls against.

-						// We override the CallMethod.invoke() method to insert our logic.

-						if ("RRPC".equals(httpMethod)) {

-

-							final ClassMeta<?> interfaceClass = getClassMeta(mi.inner().getGenericReturnType());

-							final RrpcInterfaceMeta rim = new RrpcInterfaceMeta(interfaceClass.getInnerClass(), null);

-							if (rim.getMethodsByPath().isEmpty())

-								throw new InternalServerError("Method {0} returns an interface {1} that doesn't define any remote methods.", mi.getSignature(), interfaceClass.getFullName());

-

-							RestMethodContextBuilder smb = new RestMethodContextBuilder(r, mi.inner(), this);

-							smb.dotAll();

-							sm = new RestMethodContext(smb) {

-

-								@Override

-								void invoke(RestCall call) throws Throwable {

-

-									super.invoke(call);

-

-									final Object o = call.getOutput();

-

-									if ("GET".equals(call.getMethod())) {

-										call.output(rim.getMethodsByPath().keySet());

-										return;

-

-									} else if ("POST".equals(call.getMethod())) {

-										String pip = call.getUrlPath().getPath();

-										if (pip.indexOf('/') != -1)

-											pip = pip.substring(pip.lastIndexOf('/')+1);

-										pip = urlDecode(pip);

-										RrpcInterfaceMethodMeta rmm = rim.getMethodMetaByPath(pip);

-										if (rmm != null) {

-											Method m = rmm.getJavaMethod();

-											try {

-												RestRequest req = call.getRestRequest();

-												// Parse the args and invoke the method.

-												Parser p = req.getBody().getParser();

-												Object[] args = null;

-												if (m.getGenericParameterTypes().length == 0)

-													args = new Object[0];

-												else {

-													try (Closeable in = p.isReaderParser() ? req.getReader() : req.getInputStream()) {

-														args = p.parseArgs(in, m.getGenericParameterTypes());

-													}

-												}

-												Object output = m.invoke(o, args);

-												call.output(output);

-												return;

-											} catch (Exception e) {

-												throw toHttpException(e, InternalServerError.class);

-											}

-										}

-									}

-									throw new NotFound();

-								}

-							};

-

-							methodMapBuilder.add("GET", sm).add("POST", sm);

-

-						} else {

-							methodMapBuilder.add(httpMethod, sm);

-						}

-					} catch (Throwable e) {

-						throw new RestServletException(e, "Problem occurred trying to initialize methods on class {0}, methods={1}", rci.inner().getName(), SimpleJsonSerializer.DEFAULT.serialize(methodsFound));

-					}

-				}

-			}

-

-			this.methodMap = methodMapBuilder.getMap();

-			this.methods = methodMapBuilder.getList();

 

 			// Initialize our child resources.

 			for (Object o : getArrayProperty(REST_children, Object.class)) {

@@ -4648,6 +4548,61 @@
 	}

 

 	/**

+	 * Creates the set of {@link RestMethodContext} objects that represent the methods on this resource.

+	 *

+	 * @param resource The REST resource object.

+	 * @return The builder for the {@link RestMethods} object.

+	 * @throws Exception An error occurred.

+	 */

+	protected RestMethodsBuilder createRestMethods(Object resource) throws Exception {

+		RestMethodsBuilder x = new RestMethodsBuilder();

+		ClassInfo rci = ClassInfo.of(resource);

+

+		for (MethodInfo mi : rci.getPublicMethods()) {

+			RestMethod a = mi.getLastAnnotation(RestMethod.class);

+

+			// Also include methods on @Rest-annotated interfaces.

+			if (a == null) {

+				for (Method mi2 : mi.getMatching()) {

+					Class<?> ci2 = mi2.getDeclaringClass();

+					if (ci2.isInterface() && ci2.getAnnotation(Rest.class) != null) {

+						a = RestMethodAnnotation.DEFAULT;

+					}

+				}

+			}

+			if (a != null) {

+				try {

+					if (mi.isNotPublic())

+						throw new RestServletException("@RestMethod method {0}.{1} must be defined as public.", rci.inner().getName(), mi.getSimpleName());

+

+					RestMethodContextBuilder rmcb = new RestMethodContextBuilder(resource, mi.inner(), this);

+					RestMethodContext rmc = rmcb.build();

+					String httpMethod = rmc.getHttpMethod();

+

+					// RRPC is a special case where a method returns an interface that we

+					// can perform REST calls against.

+					// We override the CallMethod.invoke() method to insert our logic.

+					if ("RRPC".equals(httpMethod)) {

+

+						RestMethodContextBuilder smb = new RestMethodContextBuilder(resource, mi.inner(), this);

+						smb.dotAll();

+						x

+							.add("GET", smb.build(RrpcRestMethodContext.class))

+							.add("POST", smb.build(RrpcRestMethodContext.class));

+

+					} else {

+						x.add(rmc);

+					}

+				} catch (Throwable e) {

+					throw new RestServletException(e, "Problem occurred trying to initialize methods on class {0}", rci.inner().getName());

+				}

+			}

+		}

+

+		return x;

+	}

+

+	/**

 	 * Instantiates the list of {@link HookEvent#START_CALL} methods.

 	 *

 	 * @param resource The REST resource object.

@@ -5341,7 +5296,7 @@
 	 * 	An unmodifiable map of Java method names to call method objects.

 	 */

 	public List<RestMethodContext> getMethodContexts() {

-		return methods;

+		return restMethods.getMethodContexts();

 	}

 

 	/**

@@ -5572,7 +5527,7 @@
 

 			// If the specified method has been defined in a subclass, invoke it.

 			try {

-				findMethod(call).invoke(call);

+				restMethods.findMethod(call).invoke(call);

 			} catch (NotFound e) {

 				if (call.getStatus() == 0)

 					call.status(404);

@@ -5597,46 +5552,6 @@
 		finishCall(call);

 	}

 

-	private RestMethodContext findMethod(RestCall call) throws Throwable {

-		String m = call.getMethod();

-

-		int rc = 0;

-		if (methodMap.containsKey(m)) {

-			for (RestMethodContext mc : methodMap.get(m)) {

-				int mrc = mc.match(call);

-				if (mrc == 2)

-					return mc;

-				rc = Math.max(rc, mrc);

-			}

-		}

-

-		if (methodMap.containsKey("*")) {

-			for (RestMethodContext mc : methodMap.get("*")) {

-				int mrc = mc.match(call);

-				if (mrc == 2)

-					return mc;

-				rc = Math.max(rc, mrc);

-			}

-		}

-

-		// If no paths matched, see if the path matches any other methods.

-		// Note that we don't want to match against "/*" patterns such as getOptions().

-		if (rc == 0) {

-			for (RestMethodContext mc : methods) {

-				if (! mc.getPathPattern().endsWith("/*")) {

-					int mrc = mc.match(call);

-					if (mrc == 2)

-						throw new MethodNotAllowed();

-				}

-			}

-		}

-

-		if (rc == 1)

-			throw new PreconditionFailed("Method ''{0}'' not found on resource on path ''{1}'' with matching matcher.", m, call.getPathInfo());

-

-		throw new NotFound("Java method matching path ''{0}'' not found on resource ''{1}''.", call.getPathInfo(), getResource().getClass().getName());

-	}

-

 	private boolean isDebug(RestCall call) {

 		Enablement e = null;

 		RestMethodContext mc = call.getRestMethodContext();

@@ -6071,29 +5986,4 @@
 	// Helpers.

 	//-----------------------------------------------------------------------------------------------------------------

 

-	static class MethodMapBuilder  {

-		TreeMap<String,TreeSet<RestMethodContext>> map = new TreeMap<>();

-		Set<RestMethodContext> set = ASet.of();

-

-

-		MethodMapBuilder add(String httpMethodName, RestMethodContext mc) {

-			httpMethodName = httpMethodName.toUpperCase();

-			if (! map.containsKey(httpMethodName))

-				map.put(httpMethodName, new TreeSet<>());

-			map.get(httpMethodName).add(mc);

-			set.add(mc);

-			return this;

-		}

-

-		Map<String,List<RestMethodContext>> getMap() {

-			AMap<String,List<RestMethodContext>> m = AMap.create();

-			for (Map.Entry<String,TreeSet<RestMethodContext>> e : map.entrySet())

-				m.put(e.getKey(), AList.of(e.getValue()));

-			return m.unmodifiable();

-		}

-

-		List<RestMethodContext> getList() {

-			return AList.of(set).unmodifiable();

-		}

-	}

 }

diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
index c6e6681..0a68e4d 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
@@ -30,7 +30,6 @@
 import java.util.function.*;
 
 import javax.servlet.*;
-import javax.servlet.http.*;
 
 import org.apache.http.*;
 import org.apache.http.ParseException;
@@ -636,15 +635,22 @@
 	final Enablement debug;
 	final int hierarchyDepth;
 
-	RestMethodContext(RestMethodContextBuilder b) throws ServletException {
-		super(b.getPropertyStore());
+	/**
+	 * Context constructor.
+	 *
+	 * @param ps The property store with settings.
+	 * @throws ServletException If context could not be created.
+	 */
+	public RestMethodContext(PropertyStore ps) throws ServletException {
+		super(ps);
 
 		try {
-			context = b.context;
-			method = b.method;
+			context = getInstanceProperty("RestMethodContext.restContext.o", RestContext.class);
+			method = getInstanceProperty("RestMethodContext.restMethod.o", Method.class);
+			boolean dotAll = getBooleanProperty("RestMethodContext.dotAll.b", false);
+
 			methodInvoker = new MethodInvoker(method, context.getMethodExecStats(method));
 			mi = MethodInfo.of(method).accessible();
-			PropertyStore ps = getPropertyStore();
 			Object r = context.getResource();
 
 			beanFactory = new BeanFactory(context.rootBeanFactory, r)
@@ -675,7 +681,7 @@
  			requiredMatchers = matchers.stream().filter(x -> x.required()).toArray(RestMatcher[]::new);
 			optionalMatchers = matchers.stream().filter(x -> ! x.required()).toArray(RestMatcher[]::new);
 
-			pathMatchers = createPathMatchers(r, beanFactory, b.dotAll).asArray();
+			pathMatchers = createPathMatchers(r, beanFactory, dotAll).asArray();
 			beanFactory.addBean(UrlPathMatcher[].class, pathMatchers);
 			beanFactory.addBean(UrlPathMatcher.class, pathMatchers.length > 0 ? pathMatchers[0] : null);
 
@@ -695,7 +701,7 @@
 			defaultRequestAttributes = createDefaultRequestAttributes(r, beanFactory, method, context).asArray();
 
 			int _hierarchyDepth = 0;
-			Class<?> sc = b.method.getDeclaringClass().getSuperclass();
+			Class<?> sc = method.getDeclaringClass().getSuperclass();
 			while (sc != null) {
 				_hierarchyDepth++;
 				sc = sc.getSuperclass();
@@ -1299,7 +1305,7 @@
 		HeaderList x = HeaderList.create();
 
 		x.appendUnique(context.defaultRequestHeaders);
-		
+
 		x.appendUnique(getInstanceArrayProperty(RESTMETHOD_defaultRequestHeaders, org.apache.http.Header.class, new org.apache.http.Header[0], beanFactory));
 
 		for (Annotation[] aa : method.getParameterAnnotations()) {
@@ -1344,7 +1350,7 @@
 		HeaderList x = HeaderList.create();
 
 		x.appendUnique(context.defaultResponseHeaders);
-		
+
 		x.appendUnique(getInstanceArrayProperty(RESTMETHOD_defaultResponseHeaders, org.apache.http.Header.class, new org.apache.http.Header[0], beanFactory));
 
 		x = BeanFactory
@@ -1372,7 +1378,7 @@
 		NamedAttributeList x = NamedAttributeList.create();
 
 		x.appendUnique(context.defaultRequestAttributes);
-		
+
 		x.appendUnique(getInstanceArrayProperty(RESTMETHOD_defaultRequestAttributes, NamedAttribute.class, new NamedAttribute[0], beanFactory));
 
 		x = BeanFactory
@@ -1619,13 +1625,13 @@
 		return pm;
 	}
 
-
 	/**
 	 * Workhorse method.
 	 *
-	 * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta)
+	 * @param call Invokes the specified call against this Java method.
+	 * @throws Throwable Typically an HTTP exception.  Anything else will result in an HTTP 500.
 	 */
-	void invoke(RestCall call) throws Throwable {
+	protected void invoke(RestCall call) throws Throwable {
 
 		UrlPathMatch pm = call.getUrlPathMatch();
 		if (pm == null)
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContextBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContextBuilder.java
index 24936ec..bd3dbda 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContextBuilder.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContextBuilder.java
@@ -19,6 +19,8 @@
 import java.util.*;
 import java.util.function.*;
 
+import javax.servlet.*;
+
 import org.apache.http.*;
 import org.apache.juneau.*;
 import org.apache.juneau.http.*;
@@ -33,14 +35,19 @@
  */
 public class RestMethodContextBuilder extends BeanContextBuilder {
 
-	RestContext context;
-	java.lang.reflect.Method method;
-
-	boolean dotAll;
+	@Override
+	public RestMethodContext build() {
+		try {
+			return new RestMethodContext(getPropertyStore());
+		} catch (ServletException e) {
+			throw new RuntimeException(e);
+		}
+	}
 
 	RestMethodContextBuilder(Object servlet, java.lang.reflect.Method method, RestContext context) throws RestServletException {
-		this.context = context;
-		this.method = method;
+		set("RestMethodContext.restContext.o", context);
+		set("RestMethodContext.restMethod.o", method);
+		set("RestMethodContext.restObject.o", context.getResource());  // Added to force a new cache hash.
 
 		String sig = method.getDeclaringClass().getName() + '.' + method.getName();
 		MethodInfo mi = MethodInfo.of(servlet.getClass(), method);
@@ -79,7 +86,7 @@
 	 * @return This object (for method chaining).
 	 */
 	public RestMethodContextBuilder dotAll() {
-		this.dotAll = true;
+		set("RestMethodContext.dotAll.b", true);
 		return this;
 	}
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethods.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethods.java
new file mode 100644
index 0000000..2690e28
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethods.java
@@ -0,0 +1,104 @@
+// ***************************************************************************************************************************
+// * 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;
+
+import java.util.*;
+
+import org.apache.juneau.collections.*;
+import org.apache.juneau.http.exception.*;
+import org.apache.juneau.rest.annotation.*;
+
+/**
+ * Encapsulates the set of {@link RestMethod}-annotated methods within a single {@link Rest}-annotated object.
+ */
+public class RestMethods {
+
+	private final Map<String,List<RestMethodContext>> map;
+	private List<RestMethodContext> list;
+
+	/**
+	 * Creates a new builder.
+	 *
+	 * @return A new builder.
+	 */
+	public static RestMethodsBuilder create() {
+		return new RestMethodsBuilder();
+	}
+
+	RestMethods(RestMethodsBuilder builder) {
+		AMap<String,List<RestMethodContext>> m = AMap.create();
+		for (Map.Entry<String,TreeSet<RestMethodContext>> e : builder.map.entrySet())
+			m.put(e.getKey(), AList.of(e.getValue()));
+		this.map = m;
+		this.list = AList.of(builder.set);
+	}
+
+	/**
+	 * Finds the method that should handle the specified call.
+	 *
+	 * @param call The HTTP call.
+	 * @return The method that should handle the specified call.
+	 * @throws MethodNotAllowed If no methods implement the requested HTTP method.
+	 * @throws PreconditionFailed At least one method was found but it didn't match one or more matchers.
+	 * @throws NotFound HTTP method match was found but matching path was not.
+	 */
+	public RestMethodContext findMethod(RestCall call) throws MethodNotAllowed, PreconditionFailed, NotFound {
+		String m = call.getMethod();
+
+		int rc = 0;
+		if (map.containsKey(m)) {
+			for (RestMethodContext mc : map.get(m)) {
+				int mrc = mc.match(call);
+				if (mrc == 2)
+					return mc;
+				rc = Math.max(rc, mrc);
+			}
+		}
+
+		if (map.containsKey("*")) {
+			for (RestMethodContext mc : map.get("*")) {
+				int mrc = mc.match(call);
+				if (mrc == 2)
+					return mc;
+				rc = Math.max(rc, mrc);
+			}
+		}
+
+		// If no paths matched, see if the path matches any other methods.
+		// Note that we don't want to match against "/*" patterns such as getOptions().
+		if (rc == 0) {
+			for (RestMethodContext mc : list) {
+				if (! mc.getPathPattern().endsWith("/*")) {
+					int mrc = mc.match(call);
+					if (mrc == 2)
+						throw new MethodNotAllowed();
+				}
+			}
+		}
+
+		if (rc == 1)
+			throw new PreconditionFailed("Method ''{0}'' not found on resource on path ''{1}'' with matching matcher.", m, call.getPathInfo());
+
+		throw new NotFound("Java method matching path ''{0}'' not found on resource ''{1}''.", call.getPathInfo(), call.getResource().getClass().getName());
+	}
+
+
+	/**
+	 * Returns the list of method contexts in this object.
+	 *
+	 * @return An unmodifiable list of method contexts in this object.
+	 */
+	public List<RestMethodContext> getMethodContexts() {
+		return Collections.unmodifiableList(list);
+	}
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodsBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodsBuilder.java
new file mode 100644
index 0000000..1c7f97f
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodsBuilder.java
@@ -0,0 +1,61 @@
+// ***************************************************************************************************************************
+// * 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;
+
+import java.util.*;
+
+import org.apache.juneau.collections.*;
+
+/**
+ * Builder for {@link RestMethods} object.
+ */
+public class RestMethodsBuilder  {
+
+	TreeMap<String,TreeSet<RestMethodContext>> map = new TreeMap<>();
+	Set<RestMethodContext> set = ASet.of();
+
+	/**
+	 * Adds a method context to this builder.
+	 *
+	 * @param mc The REST method context to add.
+	 * @return Adds a method context to this builder.
+	 */
+	public RestMethodsBuilder add(RestMethodContext mc) {
+		return add(mc.getHttpMethod(), mc);
+	}
+
+	/**
+	 * Adds a method context to this builder.
+	 *
+	 * @param httpMethodName The HTTP method name.
+	 * @param mc The REST method context to add.
+	 * @return Adds a method context to this builder.
+	 */
+	public RestMethodsBuilder add(String httpMethodName, RestMethodContext mc) {
+		httpMethodName = httpMethodName.toUpperCase();
+		if (! map.containsKey(httpMethodName))
+			map.put(httpMethodName, new TreeSet<>());
+		map.get(httpMethodName).add(mc);
+		set.add(mc);
+		return this;
+	}
+
+	/**
+	 * Creates a new {@link RestMethods} object using the contents of this builder.
+	 *
+	 * @return A new {@link RestMethods} object.
+	 */
+	public RestMethods build() {
+		return new RestMethods(this);
+	}
+}
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RrpcRestMethodContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RrpcRestMethodContext.java
new file mode 100644
index 0000000..accaafe
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RrpcRestMethodContext.java
@@ -0,0 +1,92 @@
+// ***************************************************************************************************************************
+// * 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;
+
+import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.rest.HttpRuntimeException.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+
+import javax.servlet.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.exception.*;
+import org.apache.juneau.http.remote.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * A specialized {@link RestMethodContext} for handling <js>"RRPC"</js> HTTP methods.
+ */
+public class RrpcRestMethodContext extends RestMethodContext {
+
+	private final RrpcInterfaceMeta meta;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param ps The property store containing the settings for this context.
+	 * @throws ServletException Problem with metadata was detected.
+	 */
+	public RrpcRestMethodContext(PropertyStore ps) throws ServletException {
+		super(ps);
+
+		ClassMeta<?> interfaceClass = getClassMeta(mi.inner().getGenericReturnType());
+		meta = new RrpcInterfaceMeta(interfaceClass.getInnerClass(), null);
+		if (meta.getMethodsByPath().isEmpty())
+			throw new InternalServerError("Method {0} returns an interface {1} that doesn't define any remote methods.", mi.getSignature(), interfaceClass.getFullName());
+
+	}
+
+	@Override
+	public void invoke(RestCall call) throws Throwable {
+
+		super.invoke(call);
+
+		final Object o = call.getOutput();
+
+		if ("GET".equals(call.getMethod())) {
+			call.output(meta.getMethodsByPath().keySet());
+			return;
+
+		} else if ("POST".equals(call.getMethod())) {
+			String pip = call.getUrlPath().getPath();
+			if (pip.indexOf('/') != -1)
+				pip = pip.substring(pip.lastIndexOf('/')+1);
+			pip = urlDecode(pip);
+			RrpcInterfaceMethodMeta rmm = meta.getMethodMetaByPath(pip);
+			if (rmm != null) {
+				Method m = rmm.getJavaMethod();
+				try {
+					RestRequest req = call.getRestRequest();
+					// Parse the args and invoke the method.
+					Parser p = req.getBody().getParser();
+					Object[] args = null;
+					if (m.getGenericParameterTypes().length == 0)
+						args = new Object[0];
+					else {
+						try (Closeable in = p.isReaderParser() ? req.getReader() : req.getInputStream()) {
+							args = p.parseArgs(in, m.getGenericParameterTypes());
+						}
+					}
+					Object output = m.invoke(o, args);
+					call.output(output);
+					return;
+				} catch (Exception e) {
+					throw toHttpException(e, InternalServerError.class);
+				}
+			}
+		}
+		throw new NotFound();
+	}
+}