001package org.consensusj.jsonrpc.introspection; 002 003import org.consensusj.jsonrpc.JsonRpcError; 004import org.consensusj.jsonrpc.JsonRpcErrorException; 005import org.consensusj.jsonrpc.JsonRpcException; 006import org.consensusj.jsonrpc.JsonRpcRequest; 007import org.consensusj.jsonrpc.JsonRpcResponse; 008import org.consensusj.jsonrpc.JsonRpcService; 009import org.slf4j.Logger; 010import org.slf4j.LoggerFactory; 011 012import java.lang.reflect.Method; 013import java.lang.reflect.Modifier; 014import java.util.Arrays; 015import java.util.List; 016import java.util.Map; 017import java.util.concurrent.CompletableFuture; 018import java.util.stream.Collectors; 019 020import static org.consensusj.jsonrpc.JsonRpcError.Error.METHOD_NOT_FOUND; 021import static org.consensusj.jsonrpc.JsonRpcError.Error.SERVER_EXCEPTION; 022 023/** 024 * Interface and default methods for wrapping a Java class with JSON-RPC support. The wrapper is responsible for 025 * extracting the JSON-RPC {@code method} name and {@code params}from the request, calling the appropriate method 026 * in the wrapped object and wrapping the {@code result} in a {@link JsonRpcResponse}. 027 * <p> 028 * The wrapped class must contain one or more asynchronous methods that return {@link CompletableFuture}s for objects that represent 029 * JSON-RPC {@code result} values. They are mapped to JSON objects when serialized (via <b>Jackson</b> in the current implementation.) 030 * <p> 031 * This interface contains a {@code default} implementation of {@link JsonRpcService#call(JsonRpcRequest)} that 032 * uses {@link JsonRpcServiceWrapper#callMethod(String, List)} to call the wrapped service object. 033 * <p> 034 * Implementations must implement: 035 * <ul> 036 * <li>{@link #getServiceObject()} to return the wrapped object (singleton)</li> 037 * <li>{@link #getMethod(String)} to return a {@link Method} object for the named JSON-RPC {@code method}.</li> 038 * </ul> 039 * <p> 040 * The trick to <b>GraalVM</b>-compatibility is to use the {@code static} {@link JsonRpcServiceWrapper#reflect(Class)} method in your 041 * implementation at (static) initialization time so the reflection is done at GraalVM compile-time. 042 */ 043public interface JsonRpcServiceWrapper extends JsonRpcService { 044 Logger log = LoggerFactory.getLogger(JsonRpcServiceWrapper.class); 045 046 /** 047 * Get the service object. 048 * <p>Implementations will return their configured service object here. 049 * 050 * @return the service object 051 */ 052 Object getServiceObject(); 053 054 /** 055 * Get a {@link Method} object for a named JSON-RPC method 056 * @param methodName the name of the method to call 057 * @return method handle 058 */ 059 Method getMethod(String methodName); 060 061 /** 062 * Handle a request by calling method, getting a result, and embedding it in a response. 063 * 064 * @param req The Request POJO 065 * @return A future JSON RPC Response 066 */ 067 @Override 068 default <RSLT> CompletableFuture<JsonRpcResponse<RSLT>> call(final JsonRpcRequest req) { 069 log.debug("JsonRpcServiceWrapper.call: {}", req.getMethod()); 070 CompletableFuture<RSLT> futureResult = callMethod(req.getMethod(), req.getParams()); 071 return futureResult.handle((RSLT r, Throwable ex) -> resultCompletionHandler(req, r, ex)); 072 } 073 074 /** 075 * Map a request plus a result or error into a response 076 * 077 * @param req The request being services 078 * @param result A result object or null 079 * @param ex exception or null 080 * @param <RSLT> type of result 081 * @return A success or error response as appropriate 082 */ 083 private <RSLT> JsonRpcResponse<RSLT> resultCompletionHandler(JsonRpcRequest req, RSLT result, Throwable ex) { 084 return (result != null) ? 085 wrapResult(req, result) : 086 wrapError(req, exceptionToError(ex)); 087 } 088 089 /** 090 * Map an exception (typically from callMethod) to an Error POJO 091 * 092 * @param ex Exception returned from "unwrapped" method 093 * @return An error POJO for insertion in a JsonRpcResponse 094 */ 095 private JsonRpcError exceptionToError(Throwable ex) { 096 if (ex instanceof JsonRpcErrorException) { 097 return ((JsonRpcErrorException) ex).getError(); 098 } else if (ex instanceof JsonRpcException) { 099 return JsonRpcError.of(SERVER_EXCEPTION, ex); 100 } else { 101 return JsonRpcError.of(SERVER_EXCEPTION, ex); 102 } 103 } 104 105 /** 106 * Call an "unwrapped" method from an existing service object 107 * 108 * @param methodName Method to call 109 * @param params List of JSON-RPC parameters 110 * @return A future result POJO 111 */ 112 private <RSLT> CompletableFuture<RSLT> callMethod(String methodName, List<Object> params) { 113 log.debug("JsonRpcServiceWrapper.callMethod: {}", methodName); 114 CompletableFuture<RSLT> future; 115 final Method mh = getMethod(methodName); 116 if (mh != null) { 117 try { 118 @SuppressWarnings("unchecked") 119 CompletableFuture<RSLT> liveFuture = (CompletableFuture<RSLT>) mh.invoke(getServiceObject(), params.toArray()); 120 future = liveFuture; 121 } catch (Throwable throwable) { 122 log.error("Exception in invoked service object: ", throwable); 123 JsonRpcErrorException jsonRpcException = new JsonRpcErrorException(SERVER_EXCEPTION, throwable); 124 future = CompletableFuture.failedFuture(jsonRpcException); 125 } 126 } else { 127 future = CompletableFuture.failedFuture(JsonRpcErrorException.of(METHOD_NOT_FOUND)); 128 } 129 return future; 130 } 131 132 // TODO: Create a mechanism to return a map with only the desired remotely-accessible methods in it. 133 /** 134 * Use reflection/introspection to generate a map of methods. 135 * Generally this is called to initialize a {@link Map} stored in a static field, so the reflection can be done 136 * during GraalVM compile-time.. 137 * @param apiClass The service class to reflect/introspect 138 * @return a map of method names to method objects 139 */ 140 static Map<String, Method> reflect(Class<?> apiClass) { 141 return Arrays.stream(apiClass.getMethods()) 142 .filter(m -> Modifier.isPublic(m.getModifiers())) // Only public methods 143 .filter(m -> m.getReturnType().equals(CompletableFuture.class)) // Only methods that return CompletableFuture 144 .collect(Collectors 145 .toUnmodifiableMap(Method::getName, // key is method name 146 (method) -> method, // value is Method object 147 (existingKey, key) -> key) // if duplicate, replace existing 148 ); 149 } 150 151 /** 152 * Wrap a Result POJO in a JsonRpcResponse 153 * @param req The request we are responding to 154 * @param result the result to wrap 155 * @return A valid JsonRpcResponse 156 */ 157 private static <RSLT> JsonRpcResponse<RSLT> wrapResult(JsonRpcRequest req, RSLT result) { 158 return new JsonRpcResponse<>(req, result); 159 } 160 161 private static <RSLT> JsonRpcResponse<RSLT> wrapError(JsonRpcRequest req, JsonRpcError error) { 162 return new JsonRpcResponse<>(req, error); 163 } 164}