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();
+ }
+}