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}