package com.googlecode.jsonrpc4j; import com.fasterxml.jackson.databind.node.ObjectNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import static com.googlecode.jsonrpc4j.Util.hasNonNullObjectData; import static com.googlecode.jsonrpc4j.Util.hasNonNullTextualData; /** * Default implementation of the {@link ExceptionResolver} interface that attempts to re-throw the same exception * that was thrown by the server. This always returns a {@link Throwable}. * The exception class must be present on the classpath. */ public class DefaultExceptionResolver implements ExceptionResolver { private static final Logger logger = LoggerFactory.getLogger(DefaultExceptionResolver.class); public static final DefaultExceptionResolver INSTANCE = new DefaultExceptionResolver(); /** * {@inheritDoc} */ public Throwable resolveException(ObjectNode response) { ObjectNode errorObject = ObjectNode.class.cast(response.get(JsonRpcBasicServer.ERROR)); if (!hasNonNullObjectData(errorObject, JsonRpcBasicServer.DATA)) return createJsonRpcClientException(errorObject); ObjectNode dataObject = ObjectNode.class.cast(errorObject.get(JsonRpcBasicServer.DATA)); if (!hasNonNullTextualData(dataObject, JsonRpcBasicServer.EXCEPTION_TYPE_NAME)) return createJsonRpcClientException(errorObject); try { String exceptionTypeName = dataObject.get(JsonRpcBasicServer.EXCEPTION_TYPE_NAME).asText(); String message = hasNonNullTextualData(dataObject, JsonRpcBasicServer.ERROR_MESSAGE) ? dataObject.get(JsonRpcBasicServer.ERROR_MESSAGE).asText() : null; return createThrowable(exceptionTypeName, message); } catch (Exception e) { logger.warn("Unable to create throwable", e); return createJsonRpcClientException(errorObject); } } /** * Creates a {@link JsonRpcClientException} from the given * {@link ObjectNode}. * * @param errorObject the error object * @return the exception */ private JsonRpcClientException createJsonRpcClientException(ObjectNode errorObject) { int code = errorObject.has(JsonRpcBasicServer.ERROR_CODE) ? errorObject.get(JsonRpcBasicServer.ERROR_CODE).asInt() : 0; return new JsonRpcClientException(code, errorObject.get(JsonRpcBasicServer.ERROR_MESSAGE).asText(), errorObject.get(JsonRpcBasicServer.DATA)); } /** * Attempts to create an {@link Throwable} of the given type with the given message. For this method to create a * {@link Throwable} it must have either a default (no-args) constructor or a constructor that takes a {@code String} * as the message name. null is returned if a {@link Throwable} can't be created. * * @param typeName the java type name (class name) * @param message the message * @return the throwable * @throws InvocationTargetException * @throws IllegalAccessException * @throws InstantiationException * @throws IllegalArgumentException */ private Throwable createThrowable(String typeName, String message) throws IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException { Class<? extends Throwable> clazz = resolveThrowableClass(typeName); Constructor<? extends Throwable> defaultCtr = getDefaultConstructor(clazz); Constructor<? extends Throwable> messageCtr = getMessageConstructor(clazz); if (message != null && messageCtr != null) { return messageCtr.newInstance(message); } else if (message != null && defaultCtr != null) { logger.warn("Unable to invoke message constructor for {}, fallback to default", clazz.getName()); return defaultCtr.newInstance(); } else if (message == null && defaultCtr != null) { return defaultCtr.newInstance(); } else if (message == null && messageCtr != null) { logger.warn("Passing null message to message constructor for {}", clazz.getName()); return messageCtr.newInstance((String) null); } else { logger.error("Unable to find message or default constructor for {} have {}", clazz.getName(), clazz.getDeclaredConstructors()); return null; } } /** * Resolves original exception type name into an actual {@link Class}. * Override this, if you want custom behaviour for handling exceptions, * i.e.: Default to RuntimeException. * * @param typeName Original exception type name thrown on the server. * @return the resolved throwable * @throws ClassNotFoundException - if throwable class has not been found */ protected Class<? extends Throwable> resolveThrowableClass(String typeName) throws ClassNotFoundException { Class<?> clazz; try { clazz = Class.forName(typeName); if (!Throwable.class.isAssignableFrom(clazz)) { logger.warn("Type does not inherit from Throwable {}", clazz.getName()); } else { return clazz.asSubclass(Throwable.class); } } catch(ClassNotFoundException e) { logger.warn("Unable to load Throwable class {}", typeName); throw e; } catch(Exception e) { logger.warn("Unable to load Throwable class {}", typeName); } return null; } private Constructor<? extends Throwable> getDefaultConstructor(Class<? extends Throwable> clazz) { Constructor<? extends Throwable> defaultCtr = null; try { defaultCtr = clazz.getConstructor(); } catch (NoSuchMethodException e) { handleException(e); } return defaultCtr; } private Constructor<? extends Throwable> getMessageConstructor(Class<? extends Throwable> clazz) { Constructor<? extends Throwable> messageCtr = null; try { messageCtr = clazz.getConstructor(String.class); } catch (NoSuchMethodException e) { handleException(e); } return messageCtr; } @SuppressWarnings("UnusedParameters") private void handleException(Exception e) { /* do nothing */ } }