This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push: new b6bed70 Throwables not always thrown from @Remote methods. b6bed70 is described below commit b6bed7012babe1ec941efb9da4ce7420f335dd3e Author: JamesBognar <james.bog...@salesforce.com> AuthorDate: Mon Jun 15 12:43:24 2020 -0400 Throwables not always thrown from @Remote methods. --- .../org/apache/juneau/internal/ThrowableUtils.java | 32 ++++ .../rest/test/client/ThirdPartyProxyTest.java | 6 +- .../juneau/rest/client2/InterfaceProxyTest.java | 4 +- .../apache/juneau/rest/client2/RemotesTest.java | 64 +++---- .../juneau/rest/client2/RestCallException.java | 64 ++----- .../org/apache/juneau/rest/client2/RestClient.java | 209 ++++++++++++--------- 6 files changed, 203 insertions(+), 176 deletions(-) diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ThrowableUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ThrowableUtils.java index ac7bc4b..227c8f3 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ThrowableUtils.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ThrowableUtils.java @@ -15,6 +15,7 @@ package org.apache.juneau.internal; import java.text.*; import org.apache.juneau.*; +import org.apache.juneau.reflect.*; /** * Various utility methods for creating and working with throwables. @@ -117,4 +118,35 @@ public class ThrowableUtils { } return null; } + + /** + * Given a list of available throwable types, attempts to create and throw the exception based on name. + * + * @param name The exception name. Can be a simple name or fully-qualified. + * @param message The exception message passed to the exception constructor. + * @param throwables The list of available throwable classes to choose from. + * @throws Throwable The thrown exception if it was possible to create. + */ + public static void throwException(String name, String message, Class<?>...throwables) throws Throwable { + if (name != null) + for (Class<?> t : throwables) + if (t.getName().endsWith(name)) + doThrow(t, message); + } + + private static void doThrow(Class<?> t, String msg) throws Throwable { + ConstructorInfo c = null; + ClassInfo ci = ClassInfo.of(t); + if (msg != null) { + c = ci.getPublicConstructor(String.class); + if (c != null) + throw c.<Throwable>invoke(msg); + c = ci.getPublicConstructor(Object.class); + if (c != null) + throw c.<Throwable>invoke(msg); + } + c = ci.getPublicConstructor(); + if (c != null) + throw c.<Throwable>invoke(); + } } diff --git a/juneau-microservice/juneau-microservice-ftest/src/test/java/org/apache/juneau/rest/test/client/ThirdPartyProxyTest.java b/juneau-microservice/juneau-microservice-ftest/src/test/java/org/apache/juneau/rest/test/client/ThirdPartyProxyTest.java index 2b65e84..bd7053a 100644 --- a/juneau-microservice/juneau-microservice-ftest/src/test/java/org/apache/juneau/rest/test/client/ThirdPartyProxyTest.java +++ b/juneau-microservice/juneau-microservice-ftest/src/test/java/org/apache/juneau/rest/test/client/ThirdPartyProxyTest.java @@ -66,7 +66,7 @@ public class ThirdPartyProxyTest extends RestTestcase { public ThirdPartyProxyTest(String label, Serializer serializer, Parser parser) { proxy = getCached(label, ThirdPartyProxy.class); if (proxy == null) { - this.proxy = getClient(label, serializer, parser).builder().partSerializer(UonSerializer.DEFAULT.builder().addBeanTypes().addRootType().build()).build().getRemote(ThirdPartyProxy.class, null, serializer, parser); + this.proxy = getClient(label, serializer, parser).builder().ignoreErrors().partSerializer(UonSerializer.DEFAULT.builder().addBeanTypes().addRootType().build()).build().getRemote(ThirdPartyProxy.class, null, serializer, parser); cache(label, proxy); } } @@ -2352,7 +2352,7 @@ public class ThirdPartyProxyTest extends RestTestcase { // Various primitives @RemoteMethod(method="POST", path="/setInt") - void setInt(@Body int x); + void setInt(@Body int x) throws AssertionError; @RemoteMethod(method="POST", path="/setInteger") void setInteger(@Body Integer x); @@ -2370,7 +2370,7 @@ public class ThirdPartyProxyTest extends RestTestcase { void setString(@Body String x); @RemoteMethod(method="POST", path="/setNullString") - void setNullString(@Body String x); + void setNullString(@Body String x) throws AssertionError; @RemoteMethod(method="POST", path="/setInt3dArray") String setInt3dArray(@Body int[][][] x, @org.apache.juneau.http.annotation.Query("I") int i); diff --git a/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/InterfaceProxyTest.java b/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/InterfaceProxyTest.java index f87f012..1e4b734 100644 --- a/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/InterfaceProxyTest.java +++ b/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/InterfaceProxyTest.java @@ -142,13 +142,13 @@ public class InterfaceProxyTest { // Various primitives void setNothing(); - void setInt(int x); + void setInt(int x) throws AssertionError; void setInteger(Integer x); void setBoolean(boolean x); void setFloat(float x); void setFloatObject(Float x); void setString(String x); - void setNullString(String x); + void setNullString(String x) throws AssertionError; void setInt3dArray(int[][][] x); void setInteger3dArray(Integer[][][] x); void setString3dArray(String[][][] x); diff --git a/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RemotesTest.java b/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RemotesTest.java index 068e18b..a66548a 100644 --- a/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RemotesTest.java +++ b/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RemotesTest.java @@ -374,36 +374,36 @@ public class RemotesTest { } } -// @Test -// public void c04_rethrownException() throws Exception { -// C01i x = MockRestClient -// .create(C01.class) -// .json() -// .build() -// .getRemote(C01i.class); -// -// try { -// x.c04(); -// fail(); -// } catch (C01Exception e) { -// assertEquals("foo", e.getLocalizedMessage()); -// } -// } -// -// -// @Test -// public void c05_rethrownUndefinedException() throws Exception { -// C01i x = MockRestClient -// .create(C01.class) -// .json() -// .build() -// .getRemote(C01i.class); -// -// try { -// x.c05(); -// fail(); -// } catch (RuntimeException e) { -// assertEquals("foo", e.getLocalizedMessage()); -// } -// } + @Test + public void c04_rethrownException() throws Exception { + C01i x = MockRestClient + .create(C01.class) + .json() + .build() + .getRemote(C01i.class); + + try { + x.c04(); + fail(); + } catch (C01Exception e) { + assertEquals("foo", e.getLocalizedMessage()); + } + } + + + @Test + public void c05_rethrownUndefinedException() throws Exception { + C01i x = MockRestClient + .create(C01.class) + .json() + .build() + .getRemote(C01i.class); + + try { + x.c05(); + fail(); + } catch (RuntimeException e) { + assertTrue(e.getLocalizedMessage().contains("foo")); + } + } } diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestCallException.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestCallException.java index f178406..f28c76c 100644 --- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestCallException.java +++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestCallException.java @@ -23,7 +23,6 @@ import org.apache.http.*; import org.apache.http.client.*; import org.apache.http.util.*; import org.apache.juneau.internal.*; -import org.apache.juneau.reflect.*; /** * Exception representing a <c>400+</c> HTTP response code against a remote resource or other exception. @@ -37,7 +36,6 @@ public final class RestCallException extends HttpException { HttpResponseException e; private RestResponse restResponse; - @SuppressWarnings("unused") private String serverExceptionName, serverExceptionMessage, serverExceptionTrace; /** @@ -138,54 +136,30 @@ public final class RestCallException extends HttpException { } /** - * Tries to reconstruct and re-throw the server-side exception. + * Returns the value of the <js>"Exception-Name"</js> header on the response. * - * <p> - * The exception is based on the following HTTP response headers: - * <ul> - * <li><c>Exception-Name:</c> - The full class name of the exception. - * <li><c>Exception-Message:</c> - The message returned by {@link Throwable#getMessage()}. - * <li><c>Exception-Trace:</c> - The stack trace of the exception returned by {@link Throwable#printStackTrace()}. - * </ul> - * - * <p> - * Does nothing if the server-side exception could not be reconstructed. - * - * <p> - * Currently only supports <c>Throwables</c> with either a public no-arg constructor - * or a public constructor that takes in a simple string message. + * @return The value of the <js>"Exception-Name"</js> header on the response, or <jk>null</jk> if not found. + */ + public String getServerExceptionName() { + return serverExceptionName; + } + + /** + * Returns the value of the <js>"Exception-Message"</js> header on the response. * - * @param cl The classloader to use to resolve the throwable class name. - * @param throwables The possible throwables. - * @throws Throwable If the throwable could be reconstructed. + * @return The value of the <js>"Exception-Message"</js> header on the response, or <jk>null</jk> if not found. */ - protected void throwServerException(ClassLoader cl, Class<?>...throwables) throws Throwable { - if (serverExceptionName != null) { - for (Class<?> t : throwables) - if (t.getName().endsWith(serverExceptionName)) - doThrow(t, serverExceptionMessage); - try { - ClassInfo t = ClassInfo.of(cl.loadClass(serverExceptionName)); - if (t.isChildOf(RuntimeException.class) || t.isChildOf(Error.class)) - doThrow(t.inner(), serverExceptionMessage); - } catch (ClassNotFoundException e2) { /* Ignore */ } - } + public String getServerExceptionMessage() { + return serverExceptionMessage; } - private void doThrow(Class<?> t, String msg) throws Throwable { - ConstructorInfo c = null; - ClassInfo ci = ClassInfo.of(t); - if (msg != null) { - c = ci.getPublicConstructor(String.class); - if (c != null) - throw c.<Throwable>invoke(msg); - c = ci.getPublicConstructor(Object.class); - if (c != null) - throw c.<Throwable>invoke(msg); - } - c = ci.getPublicConstructor(); - if (c != null) - throw c.<Throwable>invoke(); + /** + * Returns the value of the <js>"Exception-Trace"</js> header on the response. + * + * @return The value of the <js>"Exception-Trace"</js> header on the response, or <jk>null</jk> if not found. + */ + public String getServerExceptionTrace() { + return serverExceptionTrace; } /** diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClient.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClient.java index 39d1893..e74a59c 100644 --- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClient.java +++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClient.java @@ -2969,86 +2969,89 @@ public class RestClient extends BeanContext implements HttpClient, Closeable, Re String httpMethod = rmm.getHttpMethod(); HttpPartSerializerSession s = getPartSerializerSession(); - try { - RestRequest rc = request(httpMethod, url, hasContent(httpMethod)); - - rc.serializer(serializer); - rc.parser(parser); - - for (RemoteMethodArg a : rmm.getPathArgs()) - rc.path(a.getName(), args[a.getIndex()], a.getSchema(), a.getSerializer(s)); - - for (RemoteMethodArg a : rmm.getQueryArgs()) - rc.query(a.isSkipIfEmpty() ? SKIP_IF_EMPTY_FLAGS : DEFAULT_FLAGS, a.getName(), args[a.getIndex()], a.getSchema(), a.getSerializer(s)); - - for (RemoteMethodArg a : rmm.getFormDataArgs()) - rc.formData(a.isSkipIfEmpty() ? SKIP_IF_EMPTY_FLAGS : DEFAULT_FLAGS, a.getName(), args[a.getIndex()], a.getSchema(), a.getSerializer(s)); - - for (RemoteMethodArg a : rmm.getHeaderArgs()) - rc.header(a.isSkipIfEmpty() ? SKIP_IF_EMPTY_FLAGS : DEFAULT_FLAGS, a.getName(), args[a.getIndex()], a.getSchema(), a.getSerializer(s)); - - RemoteMethodArg ba = rmm.getBodyArg(); - if (ba != null) - rc.body(args[ba.getIndex()], ba.getSchema()); - - if (rmm.getRequestArgs().length > 0) { - for (RemoteMethodBeanArg rmba : rmm.getRequestArgs()) { - RequestBeanMeta rbm = rmba.getMeta(); - Object bean = args[rmba.getIndex()]; - if (bean != null) { - for (RequestBeanPropertyMeta p : rbm.getProperties()) { - Object val = p.getGetter().invoke(bean); - HttpPartType pt = p.getPartType(); - HttpPartSerializerSession ps = p.getSerializer(s); - String pn = p.getPartName(); - HttpPartSchema schema = p.getSchema(); - EnumSet<AddFlag> flags = schema.isSkipIfEmpty() ? SKIP_IF_EMPTY_FLAGS : DEFAULT_FLAGS; - if (pt == PATH) - rc.path(pn, val, schema, p.getSerializer(s)); - else if (val != null) { - if (pt == QUERY) - rc.query(flags, pn, val, schema, ps); - else if (pt == FORMDATA) - rc.formData(flags, pn, val, schema, ps); - else if (pt == HEADER) - rc.header(flags, pn, val, schema, ps); - else /* (pt == HttpPartType.BODY) */ - rc.body(val, schema); - } + RestRequest rc = request(httpMethod, url, hasContent(httpMethod)); + + rc.serializer(serializer); + rc.parser(parser); + + for (RemoteMethodArg a : rmm.getPathArgs()) + rc.path(a.getName(), args[a.getIndex()], a.getSchema(), a.getSerializer(s)); + + for (RemoteMethodArg a : rmm.getQueryArgs()) + rc.query(a.isSkipIfEmpty() ? SKIP_IF_EMPTY_FLAGS : DEFAULT_FLAGS, a.getName(), args[a.getIndex()], a.getSchema(), a.getSerializer(s)); + + for (RemoteMethodArg a : rmm.getFormDataArgs()) + rc.formData(a.isSkipIfEmpty() ? SKIP_IF_EMPTY_FLAGS : DEFAULT_FLAGS, a.getName(), args[a.getIndex()], a.getSchema(), a.getSerializer(s)); + + for (RemoteMethodArg a : rmm.getHeaderArgs()) + rc.header(a.isSkipIfEmpty() ? SKIP_IF_EMPTY_FLAGS : DEFAULT_FLAGS, a.getName(), args[a.getIndex()], a.getSchema(), a.getSerializer(s)); + + RemoteMethodArg ba = rmm.getBodyArg(); + if (ba != null) + rc.body(args[ba.getIndex()], ba.getSchema()); + + if (rmm.getRequestArgs().length > 0) { + for (RemoteMethodBeanArg rmba : rmm.getRequestArgs()) { + RequestBeanMeta rbm = rmba.getMeta(); + Object bean = args[rmba.getIndex()]; + if (bean != null) { + for (RequestBeanPropertyMeta p : rbm.getProperties()) { + Object val = p.getGetter().invoke(bean); + HttpPartType pt = p.getPartType(); + HttpPartSerializerSession ps = p.getSerializer(s); + String pn = p.getPartName(); + HttpPartSchema schema = p.getSchema(); + EnumSet<AddFlag> flags = schema.isSkipIfEmpty() ? SKIP_IF_EMPTY_FLAGS : DEFAULT_FLAGS; + if (pt == PATH) + rc.path(pn, val, schema, p.getSerializer(s)); + else if (val != null) { + if (pt == QUERY) + rc.query(flags, pn, val, schema, ps); + else if (pt == FORMDATA) + rc.formData(flags, pn, val, schema, ps); + else if (pt == HEADER) + rc.header(flags, pn, val, schema, ps); + else /* (pt == HttpPartType.BODY) */ + rc.body(val, schema); } } } } + } - RemoteMethodReturn rmr = rmm.getReturns(); - if (rmr.isFuture()) { - return getExecutorService(true).submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - return executeRemote(rc, method, rmr); + RemoteMethodReturn rmr = rmm.getReturns(); + if (rmr.isFuture()) { + return getExecutorService(true).submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + try { + return executeRemote(interfaceClass, rc, method, rmm); + } catch (Exception e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); } - }); - } else if (rmr.isCompletableFuture()) { - CompletableFuture<Object> cf = new CompletableFuture<>(); - getExecutorService(true).submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - cf.complete(executeRemote(rc, method, rmr)); + } + }); + } else if (rmr.isCompletableFuture()) { + CompletableFuture<Object> cf = new CompletableFuture<>(); + getExecutorService(true).submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + try { + cf.complete(executeRemote(interfaceClass, rc, method, rmm)); return null; + } catch (Exception e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); } - }); - return cf; - } - - return executeRemote(rc, method, rmr); - - } catch (RestCallException e) { - // Try to throw original exception if possible. - e.throwServerException(interfaceClass.getClassLoader(), rmm.getExceptions()); - throw new RuntimeException(e); - } catch (Exception e) { - throw new RuntimeException(e); + } + }); + return cf; } + + return executeRemote(interfaceClass, rc, method, rmm); } }); } catch (Exception e) { @@ -3056,28 +3059,46 @@ public class RestClient extends BeanContext implements HttpClient, Closeable, Re } } - Object executeRemote(RestRequest rc, Method method, RemoteMethodReturn rmr) throws RestCallException { - if (rmr.getReturnValue() == RemoteReturn.NONE) { - rc.complete(); - return null; - } else if (rmr.getReturnValue() == RemoteReturn.STATUS) { - rc.ignoreErrors(); - int returnCode = rc.complete().getStatusCode(); - Class<?> rt = method.getReturnType(); - if (rt == Integer.class || rt == int.class) - return returnCode; - if (rt == Boolean.class || rt == boolean.class) - return returnCode < 400; - throw new RestCallException("Invalid return type on method annotated with @RemoteMethod(returns=HTTP_STATUS). Only integer and booleans types are valid."); - } else if (rmr.getReturnValue() == RemoteReturn.BEAN) { - rc.ignoreErrors(); - return rc.run().as(rmr.getResponseBeanMeta()); - } else { - rc.ignoreErrors(); - Object v = rc.run().getBody().as(rmr.getReturnType()); - if (v == null && method.getReturnType().isPrimitive()) - v = ClassInfo.of(method.getReturnType()).getPrimitiveDefault(); - return v; + Object executeRemote(Class<?> interfaceClass, RestRequest rc, Method method, RemoteMethodMeta rmm) throws Throwable { + RemoteMethodReturn rmr = rmm.getReturns(); + + try { + Object ret = null; + RestResponse res = null; + if (rmr.getReturnValue() == RemoteReturn.NONE) { + res = rc.complete(); + } else if (rmr.getReturnValue() == RemoteReturn.STATUS) { + res = rc.complete(); + int returnCode = res.getStatusCode(); + Class<?> rt = method.getReturnType(); + if (rt == Integer.class || rt == int.class) + ret = returnCode; + else if (rt == Boolean.class || rt == boolean.class) + ret = returnCode < 400; + else + throw new RestCallException("Invalid return type on method annotated with @RemoteMethod(returns=HTTP_STATUS). Only integer and booleans types are valid."); + } else if (rmr.getReturnValue() == RemoteReturn.BEAN) { + rc.ignoreErrors(); + res = rc.run(); + ret = res.as(rmr.getResponseBeanMeta()); + } else { + Class<?> rt = method.getReturnType(); + if (Throwable.class.isAssignableFrom(rt)) + rc.ignoreErrors(); + res = rc.run(); + Object v = res.getBody().as(rmr.getReturnType()); + if (v == null && rt.isPrimitive()) + v = ClassInfo.of(rt).getPrimitiveDefault(); + if (rt.getName().equals(res.getStringHeader("Exception-Name"))) + res.removeHeaders("Exception-Name"); + ret = v; + } + + ThrowableUtils.throwException(res.getStringHeader("Exception-Name"), res.getStringHeader("Exception-Message"), rmm.getExceptions()); + return ret; + } catch (RestCallException e) { + ThrowableUtils.throwException(e.getServerExceptionName(), e.getServerExceptionMessage(), rmm.getExceptions()); + throw new RuntimeException(e); } } @@ -3196,7 +3217,7 @@ public class RestClient extends BeanContext implements HttpClient, Closeable, Re } catch (RestCallException e) { // Try to throw original exception if possible. - e.throwServerException(interfaceClass.getClassLoader(), method.getExceptionTypes()); + ThrowableUtils.throwException(e.getServerExceptionName(), e.getServerExceptionMessage(), method.getExceptionTypes()); throw new RuntimeException(e); } catch (Exception e) { throw new RuntimeException(e);