diff --git a/build-tools/jsonrpc/.gitignore b/build-tools/jsonrpc/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/build-tools/jsonrpc/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/build-tools/jsonrpc/build.gradle b/build-tools/jsonrpc/build.gradle new file mode 100644 index 000000000..ad1b770a7 --- /dev/null +++ b/build-tools/jsonrpc/build.gradle @@ -0,0 +1,30 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +plugins { + id 'java-library' +} + +java { + sourceCompatibility = rootProject.ext.javaSourceVersion + targetCompatibility = rootProject.ext.javaTargetVersion +} + +dependencies { + implementation google.gson + implementation project (path: ':build-tools:logger') +} \ No newline at end of file diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/CancelChecker.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/CancelChecker.java new file mode 100644 index 000000000..330276b9b --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/CancelChecker.java @@ -0,0 +1,39 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import java.util.concurrent.CancellationException; + +/** + * Used for processing requests with cancellation support. + */ +public interface CancelChecker { + + /** + * Throw a {@link CancellationException} if the currently processed request + * has been canceled. + */ + void checkCanceled(); + + /** + * Check for cancellation without throwing an exception. + */ + default boolean isCanceled() { + try { + checkCanceled(); + } catch (CancellationException ce) { + return true; + } + return false; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/CompletableFutures.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/CompletableFutures.java new file mode 100644 index 000000000..44682f9d8 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/CompletableFutures.java @@ -0,0 +1,69 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; + +public final class CompletableFutures { + private CompletableFutures() {} + + /** + * A utility method to create a {@link CompletableFuture} with cancellation support. + * + * @param code a function that accepts a {@link CancelChecker} and returns the to be computed value + * @return a future + */ + public static CompletableFuture computeAsync(Function code) { + CompletableFuture start = new CompletableFuture<>(); + CompletableFuture result = start.thenApplyAsync(code); + start.complete(new FutureCancelChecker(result)); + return result; + } + + /** + * A utility method to create a {@link CompletableFuture} with cancellation support. + * + * @param code a function that accepts a {@link CancelChecker} and returns the to be computed value + * @return a future + */ + public static CompletableFuture computeAsync(Executor executor, Function code) { + CompletableFuture start = new CompletableFuture<>(); + CompletableFuture result = start.thenApplyAsync(code, executor); + start.complete(new FutureCancelChecker(result)); + return result; + } + + public static class FutureCancelChecker implements CancelChecker { + + private final CompletableFuture future; + + public FutureCancelChecker(CompletableFuture future) { + this.future = future; + } + + @Override + public void checkCanceled() { + if (future.isCancelled()) + throw new CancellationException(); + } + + @Override + public boolean isCanceled() { + return future.isCancelled(); + } + + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/Endpoint.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/Endpoint.java new file mode 100644 index 000000000..82cac6c7d --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/Endpoint.java @@ -0,0 +1,25 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import java.util.concurrent.CompletableFuture; + +/** + * An endpoint is a generic interface that accepts jsonrpc requests and notifications. + */ +public interface Endpoint { + + CompletableFuture request(String method, Object parameter); + + void notify(String method, Object parameter); + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/JsonRpcException.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/JsonRpcException.java new file mode 100644 index 000000000..7cbc786aa --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/JsonRpcException.java @@ -0,0 +1,44 @@ +/****************************************************************************** + * Copyright (c) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.SocketException; +import java.nio.channels.ClosedChannelException; + +/** + * An exception thrown when accessing the JSON-RPC communication channel fails. + */ +public class JsonRpcException extends RuntimeException { + + /** + * Whether the given exception indicates that the currently accessed stream has been closed. + */ + public static boolean indicatesStreamClosed(Throwable thr) { + return thr instanceof InterruptedIOException + || thr instanceof ClosedChannelException + || thr instanceof IOException && "Pipe closed".equals(thr.getMessage()) + || thr instanceof SocketException && + ("Connection reset".equals(thr.getMessage()) || + "Socket closed".equals(thr.getMessage()) || + "Broken pipe (Write failed)".equals(thr.getMessage())) + || thr instanceof JsonRpcException && indicatesStreamClosed(thr.getCause()); + } + + private static final long serialVersionUID = -7952794305289314670L; + + public JsonRpcException(Throwable cause) { + super(cause); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/Launcher.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/Launcher.java new file mode 100644 index 000000000..b543a739e --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/Launcher.java @@ -0,0 +1,442 @@ +/****************************************************************************** + * Copyright (c) 2016-2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor; +import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod; +import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethodProvider; +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.json.StreamMessageConsumer; +import org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.services.ServiceEndpoints; +import org.eclipse.lsp4j.jsonrpc.validation.ReflectiveMessageValidator; + +import com.google.gson.GsonBuilder; + +/** + * This is the entry point for applications that use LSP4J. A Launcher does all the wiring that is necessary to connect + * your endpoint via an input stream and an output stream. + * + * @param remote service interface type + */ +public interface Launcher { + + /** + * Create a new Launcher for a given local service object, a given remote interface and an input and output stream. + * + * @param localService - the object that receives method calls from the remote service + * @param remoteInterface - an interface on which RPC methods are looked up + * @param in - input stream to listen for incoming messages + * @param out - output stream to send outgoing messages + */ + static Launcher createLauncher(Object localService, Class remoteInterface, InputStream in, OutputStream out) { + return new Builder() + .setLocalService(localService) + .setRemoteInterface(remoteInterface) + .setInput(in) + .setOutput(out) + .create(); + } + + /** + * Create a new Launcher for a given local service object, a given remote interface and an input and output stream, + * and set up message validation and tracing. + * + * @param localService - the object that receives method calls from the remote service + * @param remoteInterface - an interface on which RPC methods are looked up + * @param in - input stream to listen for incoming messages + * @param out - output stream to send outgoing messages + * @param validate - whether messages should be validated with the {@link ReflectiveMessageValidator} + * @param trace - a writer to which incoming and outgoing messages are traced, or {@code null} to disable tracing + */ + static Launcher createLauncher(Object localService, Class remoteInterface, InputStream in, OutputStream out, + boolean validate, PrintWriter trace) { + return new Builder() + .setLocalService(localService) + .setRemoteInterface(remoteInterface) + .setInput(in) + .setOutput(out) + .validateMessages(validate) + .traceMessages(trace) + .create(); + } + + /** + * Create a new Launcher for a given local service object, a given remote interface and an input and output stream. + * Threads are started with the given executor service. The wrapper function is applied to the incoming and + * outgoing message streams so additional message handling such as validation and tracing can be included. + * + * @param localService - the object that receives method calls from the remote service + * @param remoteInterface - an interface on which RPC methods are looked up + * @param in - input stream to listen for incoming messages + * @param out - output stream to send outgoing messages + * @param executorService - the executor service used to start threads + * @param wrapper - a function for plugging in additional message consumers + */ + static Launcher createLauncher(Object localService, Class remoteInterface, InputStream in, OutputStream out, + ExecutorService executorService, Function wrapper) { + return createIoLauncher(localService, remoteInterface, in, out, executorService, wrapper); + } + + /** + * Create a new Launcher for a given local service object, a given remote interface and an input and output stream. + * Threads are started with the given executor service. The wrapper function is applied to the incoming and + * outgoing message streams so additional message handling such as validation and tracing can be included. + * + * @param localService - the object that receives method calls from the remote service + * @param remoteInterface - an interface on which RPC methods are looked up + * @param in - input stream to listen for incoming messages + * @param out - output stream to send outgoing messages + * @param executorService - the executor service used to start threads + * @param wrapper - a function for plugging in additional message consumers + */ + static Launcher createIoLauncher(Object localService, Class remoteInterface, InputStream in, OutputStream out, + ExecutorService executorService, Function wrapper) { + return new Builder() + .setLocalService(localService) + .setRemoteInterface(remoteInterface) + .setInput(in) + .setOutput(out) + .setExecutorService(executorService) + .wrapMessages(wrapper) + .create(); + } + + /** + * Create a new Launcher for a given local service object, a given remote interface and an input and output stream. + * Threads are started with the given executor service. The wrapper function is applied to the incoming and + * outgoing message streams so additional message handling such as validation and tracing can be included. + * The {@code configureGson} function can be used to register additional type adapters in the {@link GsonBuilder} + * in order to support protocol classes that cannot be handled by Gson's reflective capabilities. + * + * @param localService - the object that receives method calls from the remote service + * @param remoteInterface - an interface on which RPC methods are looked up + * @param in - input stream to listen for incoming messages + * @param out - output stream to send outgoing messages + * @param executorService - the executor service used to start threads + * @param wrapper - a function for plugging in additional message consumers + * @param configureGson - a function for Gson configuration + */ + static Launcher createIoLauncher(Object localService, Class remoteInterface, InputStream in, OutputStream out, + ExecutorService executorService, Function wrapper, Consumer configureGson) { + return new Builder() + .setLocalService(localService) + .setRemoteInterface(remoteInterface) + .setInput(in) + .setOutput(out) + .setExecutorService(executorService) + .wrapMessages(wrapper) + .configureGson(configureGson) + .create(); + } + + /** + * Create a new Launcher for a given local service object, a given remote interface and an input and output stream. + * Threads are started with the given executor service. The wrapper function is applied to the incoming and + * outgoing message streams so additional message handling such as validation and tracing can be included. + * The {@code configureGson} function can be used to register additional type adapters in the {@link GsonBuilder} + * in order to support protocol classes that cannot be handled by Gson's reflective capabilities. + * + * @param localService - the object that receives method calls from the remote service + * @param remoteInterface - an interface on which RPC methods are looked up + * @param in - input stream to listen for incoming messages + * @param out - output stream to send outgoing messages + * @param validate - whether messages should be validated with the {@link ReflectiveMessageValidator} + * @param executorService - the executor service used to start threads + * @param wrapper - a function for plugging in additional message consumers + * @param configureGson - a function for Gson configuration + */ + static Launcher createIoLauncher(Object localService, Class remoteInterface, InputStream in, OutputStream out, boolean validate, + ExecutorService executorService, Function wrapper, Consumer configureGson) { + return new Builder() + .setLocalService(localService) + .setRemoteInterface(remoteInterface) + .setInput(in) + .setOutput(out) + .validateMessages(validate) + .setExecutorService(executorService) + .wrapMessages(wrapper) + .configureGson(configureGson) + .create(); + } + + /** + * Create a new Launcher for a collection of local service objects, a collection of remote interfaces and an + * input and output stream. Threads are started with the given executor service. The wrapper function is applied + * to the incoming and outgoing message streams so additional message handling such as validation and tracing + * can be included. The {@code configureGson} function can be used to register additional type adapters in + * the {@link GsonBuilder} in order to support protocol classes that cannot be handled by Gson's reflective + * capabilities. + * + * @param localServices - the objects that receive method calls from the remote services + * @param remoteInterfaces - interfaces on which RPC methods are looked up + * @param classLoader - a class loader that is able to resolve all given interfaces + * @param in - input stream to listen for incoming messages + * @param out - output stream to send outgoing messages + * @param executorService - the executor service used to start threads + * @param wrapper - a function for plugging in additional message consumers + * @param configureGson - a function for Gson configuration + */ + static Launcher createIoLauncher(Collection localServices, Collection> remoteInterfaces, ClassLoader classLoader, + InputStream in, OutputStream out, ExecutorService executorService, Function wrapper, + Consumer configureGson) { + return new Builder() + .setLocalServices(localServices) + .setRemoteInterfaces(remoteInterfaces) + .setClassLoader(classLoader) + .setInput(in) + .setOutput(out) + .setExecutorService(executorService) + .wrapMessages(wrapper) + .configureGson(configureGson) + .create(); + } + + + //------------------------------ Builder Class ------------------------------// + + /** + * The launcher builder wires up all components for JSON-RPC communication. + * + * @param remote service interface type + */ + public static class Builder { + + protected Collection localServices; + protected Collection> remoteInterfaces; + protected InputStream input; + protected OutputStream output; + protected ExecutorService executorService; + protected Function messageWrapper; + protected Function exceptionHandler; + protected boolean validateMessages; + protected Consumer configureGson; + protected ClassLoader classLoader; + protected MessageTracer messageTracer; + + public Builder setLocalService(Object localService) { + this.localServices = Collections.singletonList(localService); + return this; + } + + public Builder setLocalServices(Collection localServices) { + this.localServices = localServices; + return this; + } + + public Builder setRemoteInterface(Class remoteInterface) { + this.remoteInterfaces = Collections.singletonList(remoteInterface); + return this; + } + + public Builder setRemoteInterfaces(Collection> remoteInterfaces) { + this.remoteInterfaces = remoteInterfaces; + return this; + } + + public Builder setInput(InputStream input) { + this.input = input; + return this; + } + + public Builder setOutput(OutputStream output) { + this.output = output; + return this; + } + + public Builder setExecutorService(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + public Builder setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + return this; + } + + public Builder wrapMessages(Function wrapper) { + this.messageWrapper = wrapper; + return this; + } + + public Builder setExceptionHandler(Function exceptionHandler) { + this.exceptionHandler = exceptionHandler; + return this; + } + + public Builder validateMessages(boolean validate) { + this.validateMessages = validate; + return this; + } + + public Builder traceMessages(PrintWriter tracer) { + if (tracer != null) { + this.messageTracer = new MessageTracer(tracer); + } + return this; + } + + public Builder configureGson(Consumer configureGson) { + this.configureGson = configureGson; + return this; + } + + public Launcher create() { + // Validate input + if (input == null) + throw new IllegalStateException("Input stream must be configured."); + if (output == null) + throw new IllegalStateException("Output stream must be configured."); + if (localServices == null) + throw new IllegalStateException("Local service must be configured."); + if (remoteInterfaces == null) + throw new IllegalStateException("Remote interface must be configured."); + + // Create the JSON handler, remote endpoint and remote proxy + MessageJsonHandler jsonHandler = createJsonHandler(); + RemoteEndpoint remoteEndpoint = createRemoteEndpoint(jsonHandler); + T remoteProxy = createProxy(remoteEndpoint); + + // Create the message processor + StreamMessageProducer reader = new StreamMessageProducer(input, jsonHandler, remoteEndpoint); + MessageConsumer messageConsumer = wrapMessageConsumer(remoteEndpoint); + ConcurrentMessageProcessor msgProcessor = createMessageProcessor(reader, messageConsumer, remoteProxy); + ExecutorService execService = executorService != null ? executorService : Executors.newCachedThreadPool(); + return createLauncher(execService, remoteProxy, remoteEndpoint, msgProcessor); + } + + /** + * Create the JSON handler for messages between the local and remote services. + */ + protected MessageJsonHandler createJsonHandler() { + Map supportedMethods = getSupportedMethods(); + if (configureGson != null) + return new MessageJsonHandler(supportedMethods, configureGson); + else + return new MessageJsonHandler(supportedMethods); + } + + /** + * Create the remote endpoint that communicates with the local services. + */ + protected RemoteEndpoint createRemoteEndpoint(MessageJsonHandler jsonHandler) { + MessageConsumer outgoingMessageStream = new StreamMessageConsumer(output, jsonHandler); + outgoingMessageStream = wrapMessageConsumer(outgoingMessageStream); + Endpoint localEndpoint = ServiceEndpoints.toEndpoint(localServices); + RemoteEndpoint remoteEndpoint; + if (exceptionHandler == null) + remoteEndpoint = new RemoteEndpoint(outgoingMessageStream, localEndpoint); + else + remoteEndpoint = new RemoteEndpoint(outgoingMessageStream, localEndpoint, exceptionHandler); + jsonHandler.setMethodProvider(remoteEndpoint); + return remoteEndpoint; + } + + /** + * Create the proxy for calling methods on the remote service. + */ + @SuppressWarnings("unchecked") + protected T createProxy(RemoteEndpoint remoteEndpoint) { + if (localServices.size() == 1 && remoteInterfaces.size() == 1) { + return ServiceEndpoints.toServiceObject(remoteEndpoint, remoteInterfaces.iterator().next()); + } else { + return (T) ServiceEndpoints.toServiceObject(remoteEndpoint, (Collection>) (Object) remoteInterfaces, classLoader); + } + } + + /** + * Create the message processor that listens to the input stream. + */ + protected ConcurrentMessageProcessor createMessageProcessor(MessageProducer reader, + MessageConsumer messageConsumer, T remoteProxy) { + return new ConcurrentMessageProcessor(reader, messageConsumer); + } + + protected Launcher createLauncher(ExecutorService execService, T remoteProxy, RemoteEndpoint remoteEndpoint, + ConcurrentMessageProcessor msgProcessor) { + return new StandardLauncher(execService, remoteProxy, remoteEndpoint, msgProcessor); + } + + protected MessageConsumer wrapMessageConsumer(MessageConsumer consumer) { + MessageConsumer result = consumer; + if (messageTracer != null) { + result = messageTracer.apply(consumer); + } + if (validateMessages) { + result = new ReflectiveMessageValidator(result); + } + if (messageWrapper != null) { + result = messageWrapper.apply(result); + } + return result; + } + + /** + * Gather all JSON-RPC methods from the local and remote services. + */ + protected Map getSupportedMethods() { + Map supportedMethods = new LinkedHashMap<>(); + // Gather the supported methods of remote interfaces + for (Class interface_ : remoteInterfaces) { + supportedMethods.putAll(ServiceEndpoints.getSupportedMethods(interface_)); + } + + // Gather the supported methods of local services + for (Object localService : localServices) { + if (localService instanceof JsonRpcMethodProvider) { + JsonRpcMethodProvider rpcMethodProvider = (JsonRpcMethodProvider) localService; + supportedMethods.putAll(rpcMethodProvider.supportedMethods()); + } else { + supportedMethods.putAll(ServiceEndpoints.getSupportedMethods(localService.getClass())); + } + } + + return supportedMethods; + } + } + + + //---------------------------- Interface Methods ----------------------------// + + /** + * Start a thread that listens to the input stream. The thread terminates when the stream is closed. + * + * @return a future that returns {@code null} when the listener thread is terminated + */ + Future startListening(); + + /** + * Returns the proxy instance that implements the remote service interfaces. + */ + T getRemoteProxy(); + + /** + * Returns the remote endpoint. Use this one to send generic {@code request} or {@code notify} methods + * to the remote services. + */ + RemoteEndpoint getRemoteEndpoint(); + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageConsumer.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageConsumer.java new file mode 100644 index 000000000..eb494658b --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageConsumer.java @@ -0,0 +1,26 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import org.eclipse.lsp4j.jsonrpc.messages.Message; + +public interface MessageConsumer { + + /** + * Consume a single message. + * + * @throws MessageIssueException when an issue is found that prevents further processing of the message + * @throws JsonRpcException when accessing the JSON-RPC communication channel fails + */ + void consume(Message message) throws MessageIssueException, JsonRpcException; + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageIssueException.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageIssueException.java new file mode 100644 index 000000000..58dec38a4 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageIssueException.java @@ -0,0 +1,64 @@ +/****************************************************************************** + * Copyright (c) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.MessageIssue; + +/** + * An exception thrown to notify the caller of a {@link MessageConsumer} that one or more issues were + * found while parsing or validating a message. This information can be passed to a {@link MessageIssueHandler} + * in order to construct a proper response. + */ +public class MessageIssueException extends RuntimeException { + + private static final long serialVersionUID = 1118210473728652711L; + + private final Message rpcMessage; + private final List issues; + + public MessageIssueException(Message rpcMessage, MessageIssue issue) { + this.rpcMessage = rpcMessage; + this.issues = Collections.singletonList(issue); + } + + public MessageIssueException(Message rpcMessage, Collection issues) { + this.rpcMessage = rpcMessage; + this.issues = Collections.unmodifiableList(new ArrayList<>(issues)); + } + + public MessageIssueException(Message rpcMessage, Stream issueStream) { + this.rpcMessage = rpcMessage; + this.issues = Collections.unmodifiableList(issueStream.collect(Collectors.toList())); + } + + @Override + public String getMessage() { + return issues.stream().map(issue -> issue.getText()).collect(Collectors.joining("\n")); + } + + public Message getRpcMessage() { + return rpcMessage; + } + + public List getIssues() { + return issues; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageIssueHandler.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageIssueHandler.java new file mode 100644 index 000000000..768465b65 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageIssueHandler.java @@ -0,0 +1,26 @@ +/****************************************************************************** + * Copyright (c) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import java.util.List; + +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.MessageIssue; + +public interface MessageIssueHandler { + + /** + * Handle issues found while parsing or validating a message. The list of issues must not be empty. + */ + void handle(Message message, List issues); + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageProducer.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageProducer.java new file mode 100644 index 000000000..30b2e5146 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageProducer.java @@ -0,0 +1,24 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +public interface MessageProducer { + + /** + * Listen to a message source and forward all messages to the given consumer. Typically this method + * blocks until the message source is unable to deliver more messages. + * + * @throws JsonRpcException when accessing the JSON-RPC communication channel fails + */ + void listen(MessageConsumer messageConsumer) throws JsonRpcException; + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageTracer.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageTracer.java new file mode 100644 index 000000000..8398f6f19 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/MessageTracer.java @@ -0,0 +1,41 @@ +/****************************************************************************** + * Copyright (c) 2016-2019 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import org.eclipse.lsp4j.jsonrpc.TracingMessageConsumer.RequestMetadata; + +import java.io.PrintWriter; +import java.time.Clock; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * Wraps a {@link MessageConsumer} with one that logs in a way that the LSP Inspector can parse. * + * https://microsoft.github.io/language-server-protocol/inspector/ + */ +public class MessageTracer implements Function { + private final PrintWriter printWriter; + private final Map sentRequests = new HashMap<>(); + private final Map receivedRequests = new HashMap<>(); + + MessageTracer(PrintWriter printWriter) { + this.printWriter = Objects.requireNonNull(printWriter); + } + + @Override + public MessageConsumer apply(MessageConsumer messageConsumer) { + return new TracingMessageConsumer( + messageConsumer, sentRequests, receivedRequests, printWriter, Clock.systemDefaultZone()); + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/RemoteEndpoint.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/RemoteEndpoint.java new file mode 100644 index 000000000..6d684a02b --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/RemoteEndpoint.java @@ -0,0 +1,417 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import com.itsaky.androidide.utils.ILogger; + +import org.eclipse.lsp4j.jsonrpc.json.MessageConstants; +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.json.MethodProvider; +import org.eclipse.lsp4j.jsonrpc.messages.CancelParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.MessageIssue; +import org.eclipse.lsp4j.jsonrpc.messages.NotificationMessage; +import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +/** + * An endpoint that can be used to send messages to a given {@link MessageConsumer} by calling + * {@link #request(String, Object)} or {@link #notify(String, Object)}. When connected to a {@link + * MessageProducer}, this class forwards received messages to the local {@link Endpoint} given in + * the constructor. + */ +public class RemoteEndpoint + implements Endpoint, MessageConsumer, MessageIssueHandler, MethodProvider { + + private static final ILogger LOG = ILogger.newInstance("RemoteEndpoint"); + + public static final Function DEFAULT_EXCEPTION_HANDLER = + (throwable) -> { + if (throwable instanceof ResponseErrorException) { + return ((ResponseErrorException) throwable).getResponseError(); + } else if ((throwable instanceof CompletionException + || throwable instanceof InvocationTargetException) + && throwable.getCause() instanceof ResponseErrorException) { + return ((ResponseErrorException) throwable.getCause()).getResponseError(); + } else { + return fallbackResponseError("Internal error", throwable); + } + }; + private final MessageConsumer out; + private final Endpoint localEndpoint; + private final Function exceptionHandler; + private final AtomicInteger nextRequestId = new AtomicInteger(); + private final Map sentRequestMap = new LinkedHashMap<>(); + private final Map> receivedRequestMap = new LinkedHashMap<>(); + /** + * @param out - a consumer that transmits messages to the remote service + * @param localEndpoint - the local service implementation + */ + public RemoteEndpoint(MessageConsumer out, Endpoint localEndpoint) { + this(out, localEndpoint, DEFAULT_EXCEPTION_HANDLER); + } + + /** + * @param out - a consumer that transmits messages to the remote service + * @param localEndpoint - the local service implementation + * @param exceptionHandler - an exception handler that should never return null. + */ + public RemoteEndpoint( + MessageConsumer out, + Endpoint localEndpoint, + Function exceptionHandler) { + if (out == null) throw new NullPointerException("out"); + if (localEndpoint == null) throw new NullPointerException("localEndpoint"); + if (exceptionHandler == null) throw new NullPointerException("exceptionHandler"); + this.out = out; + this.localEndpoint = localEndpoint; + this.exceptionHandler = exceptionHandler; + } + + private static ResponseError fallbackResponseError(String header, Throwable throwable) { + LOG.error(header + ": " + throwable.getMessage(), throwable); + ResponseError error = new ResponseError(); + error.setMessage(header + "."); + error.setCode(ResponseErrorCode.InternalError); + ByteArrayOutputStream stackTrace = new ByteArrayOutputStream(); + PrintWriter stackTraceWriter = new PrintWriter(stackTrace); + throwable.printStackTrace(stackTraceWriter); + stackTraceWriter.flush(); + error.setData(stackTrace.toString()); + return error; + } + + /** Send a request to the remote endpoint. */ + @Override + public CompletableFuture request(String method, Object parameter) { + final RequestMessage requestMessage = createRequestMessage(method, parameter); + final CompletableFuture result = + new CompletableFuture() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + sendCancelNotification(requestMessage.getRawId()); + return super.cancel(mayInterruptIfRunning); + } + }; + synchronized (sentRequestMap) { + // Store request information so it can be handled when the response is received + sentRequestMap.put(requestMessage.getId(), new PendingRequestInfo(requestMessage, result)); + } + + try { + // Send the request to the remote service + out.consume(requestMessage); + } catch (Exception exception) { + // The message could not be sent, e.g. because the communication channel was closed + result.completeExceptionally(exception); + } + return result; + } + + /** Send a notification to the remote endpoint. */ + @Override + public void notify(String method, Object parameter) { + NotificationMessage notificationMessage = createNotificationMessage(method, parameter); + try { + out.consume(notificationMessage); + } catch (Exception exception) { + int logLevel = + JsonRpcException.indicatesStreamClosed(exception) ? ILogger.INFO : ILogger.WARNING; + LOG.log(logLevel, "Failed to send notification message.", exception); + } + } + + protected RequestMessage createRequestMessage(String method, Object parameter) { + RequestMessage requestMessage = new RequestMessage(); + requestMessage.setId(String.valueOf(nextRequestId.incrementAndGet())); + requestMessage.setMethod(method); + requestMessage.setParams(parameter); + return requestMessage; + } + + protected void sendCancelNotification(Either id) { + CancelParams cancelParams = new CancelParams(); + cancelParams.setRawId(id); + notify(MessageJsonHandler.CANCEL_METHOD.getMethodName(), cancelParams); + } + + protected NotificationMessage createNotificationMessage(String method, Object parameter) { + NotificationMessage notificationMessage = new NotificationMessage(); + notificationMessage.setJsonrpc(MessageConstants.JSONRPC_VERSION); + notificationMessage.setMethod(method); + notificationMessage.setParams(parameter); + return notificationMessage; + } + + @Override + public void consume(Message message) { + if (message instanceof NotificationMessage) { + NotificationMessage notificationMessage = (NotificationMessage) message; + handleNotification(notificationMessage); + } else if (message instanceof RequestMessage) { + RequestMessage requestMessage = (RequestMessage) message; + handleRequest(requestMessage); + } else if (message instanceof ResponseMessage) { + ResponseMessage responseMessage = (ResponseMessage) message; + handleResponse(responseMessage); + } else { + LOG.warn("Unkown message type.", message); + } + } + + @Override + public void handle(Message message, List issues) { + if (issues.isEmpty()) { + throw new IllegalArgumentException("The list of issues must not be empty."); + } + + if (message instanceof RequestMessage) { + RequestMessage requestMessage = (RequestMessage) message; + handleRequestIssues(requestMessage, issues); + } else if (message instanceof ResponseMessage) { + ResponseMessage responseMessage = (ResponseMessage) message; + handleResponseIssues(responseMessage, issues); + } else { + logIssues(message, issues); + } + } + + protected void handleRequestIssues(RequestMessage requestMessage, List issues) { + ResponseError errorObject = new ResponseError(); + if (issues.size() == 1) { + MessageIssue issue = issues.get(0); + errorObject.setMessage(issue.getText()); + errorObject.setCode(issue.getIssueCode()); + errorObject.setData(issue.getCause()); + } else { + if (requestMessage.getMethod() != null) + errorObject.setMessage( + "Multiple issues were found in '" + requestMessage.getMethod() + "' request."); + else errorObject.setMessage("Multiple issues were found in request."); + errorObject.setCode(ResponseErrorCode.InvalidRequest); + errorObject.setData(issues); + } + out.consume(createErrorResponseMessage(requestMessage, errorObject)); + } + + protected void handleResponseIssues(ResponseMessage responseMessage, List issues) { + PendingRequestInfo requestInfo; + synchronized (sentRequestMap) { + requestInfo = sentRequestMap.remove(responseMessage.getId()); + } + if (requestInfo == null) { + // We have no pending request information that matches the id given in the response + LOG.warn("Unmatched response message: " + responseMessage); + logIssues(responseMessage, issues); + } else { + requestInfo.future.completeExceptionally(new MessageIssueException(responseMessage, issues)); + } + } + + protected void logIssues(Message message, List issues) { + for (MessageIssue issue : issues) { + String logMessage = + "Issue found in " + message.getClass().getSimpleName() + ": " + issue.getText(); + LOG.warn(logMessage, issue.getCause()); + } + } + + protected ResponseMessage createErrorResponseMessage( + RequestMessage requestMessage, ResponseError errorObject) { + ResponseMessage responseMessage = createResponseMessage(requestMessage); + responseMessage.setError(errorObject); + return responseMessage; + } + + protected ResponseMessage createResponseMessage(RequestMessage requestMessage) { + ResponseMessage responseMessage = new ResponseMessage(); + responseMessage.setRawId(requestMessage.getRawId()); + responseMessage.setJsonrpc(MessageConstants.JSONRPC_VERSION); + return responseMessage; + } + + @Override + public String resolveMethod(String requestId) { + synchronized (sentRequestMap) { + PendingRequestInfo requestInfo = sentRequestMap.get(requestId); + if (requestInfo != null) { + return requestInfo.requestMessage.getMethod(); + } + } + return null; + } + + protected void handleResponse(ResponseMessage responseMessage) { + PendingRequestInfo requestInfo; + synchronized (sentRequestMap) { + requestInfo = sentRequestMap.remove(responseMessage.getId()); + } + if (requestInfo == null) { + // We have no pending request information that matches the id given in the response + LOG.warn("Unmatched response message: " + responseMessage); + } else if (responseMessage.getError() != null) { + // The remote service has replied with an error + requestInfo.future.completeExceptionally( + new ResponseErrorException(responseMessage.getError())); + } else { + // The remote service has replied with a result object + requestInfo.future.complete(responseMessage.getResult()); + } + } + + protected void handleNotification(NotificationMessage notificationMessage) { + if (!handleCancellation(notificationMessage)) { + // Forward the notification to the local endpoint + try { + localEndpoint.notify(notificationMessage.getMethod(), notificationMessage.getParams()); + } catch (Exception exception) { + LOG.warn("Notification threw an exception: " + notificationMessage, exception); + } + } + } + + /** + * Cancellation is handled inside this class and not forwarded to the local endpoint. + * + * @return {@code true} if the given message is a cancellation notification, {@code false} if it + * can be handled by the local endpoint + */ + protected boolean handleCancellation(NotificationMessage notificationMessage) { + if (MessageJsonHandler.CANCEL_METHOD.getMethodName().equals(notificationMessage.getMethod())) { + Object cancelParams = notificationMessage.getParams(); + if (cancelParams != null) { + if (cancelParams instanceof CancelParams) { + synchronized (receivedRequestMap) { + String id = ((CancelParams) cancelParams).getId(); + CompletableFuture future = receivedRequestMap.get(id); + if (future != null) future.cancel(true); + else LOG.warn("Unmatched cancel notification for request id " + id); + } + return true; + } else { + LOG.warn( + "Cancellation support is disabled, since the '" + + MessageJsonHandler.CANCEL_METHOD.getMethodName() + + "' method has been registered explicitly."); + } + } else { + LOG.warn("Missing 'params' attribute of cancel notification."); + } + } + return false; + } + + protected void handleRequest(RequestMessage requestMessage) { + CompletableFuture future; + try { + // Forward the request to the local endpoint + future = localEndpoint.request(requestMessage.getMethod(), requestMessage.getParams()); + } catch (Throwable throwable) { + // The local endpoint has failed handling the request - reply with an error response + ResponseError errorObject = exceptionHandler.apply(throwable); + if (errorObject == null) { + errorObject = + fallbackResponseError( + "Internal error. Exception handler provided no error object", throwable); + } + out.consume(createErrorResponseMessage(requestMessage, errorObject)); + if (throwable instanceof Error) throw (Error) throwable; + else return; + } + + final String messageId = requestMessage.getId(); + synchronized (receivedRequestMap) { + receivedRequestMap.put(messageId, future); + } + future + .thenAccept( + (result) -> { + // Reply with the result object that was computed by the local endpoint + out.consume(createResultResponseMessage(requestMessage, result)); + }) + .exceptionally( + (Throwable t) -> { + // The local endpoint has failed computing a result - reply with an error response + ResponseMessage responseMessage; + if (isCancellation(t)) { + String message = + "The request (id: " + + messageId + + ", method: '" + + requestMessage.getMethod() + + "') has been cancelled"; + ResponseError errorObject = + new ResponseError(ResponseErrorCode.RequestCancelled, message, null); + responseMessage = createErrorResponseMessage(requestMessage, errorObject); + } else { + ResponseError errorObject = exceptionHandler.apply(t); + if (errorObject == null) { + errorObject = + fallbackResponseError( + "Internal error. Exception handler provided no error object", t); + } + responseMessage = createErrorResponseMessage(requestMessage, errorObject); + } + out.consume(responseMessage); + return null; + }) + .thenApply( + (obj) -> { + synchronized (receivedRequestMap) { + receivedRequestMap.remove(messageId); + } + return null; + }); + } + + protected ResponseMessage createResultResponseMessage( + RequestMessage requestMessage, Object result) { + ResponseMessage responseMessage = createResponseMessage(requestMessage); + responseMessage.setResult(result); + return responseMessage; + } + + protected boolean isCancellation(Throwable t) { + if (t instanceof CompletionException) { + return isCancellation(t.getCause()); + } + return (t instanceof CancellationException); + } + + /** + * Information about requests that have been sent and for which no response has been received yet. + */ + private static class PendingRequestInfo { + RequestMessage requestMessage; + CompletableFuture future; + + PendingRequestInfo(RequestMessage requestMessage2, CompletableFuture future2) { + this.requestMessage = requestMessage2; + this.future = future2; + } + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/ResponseErrorException.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/ResponseErrorException.java new file mode 100644 index 000000000..d700591f2 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/ResponseErrorException.java @@ -0,0 +1,36 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; + +/** + * An exception thrown in order to send a response with an attached {@code error} object. + */ +public class ResponseErrorException extends RuntimeException { + + private static final long serialVersionUID = -5970739895395246885L; + private ResponseError responseError; + + public ResponseErrorException(ResponseError responseError) { + this.responseError = responseError; + } + + @Override + public String getMessage() { + return responseError.getMessage(); + } + + public ResponseError getResponseError() { + return responseError; + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/StandardLauncher.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/StandardLauncher.java new file mode 100644 index 000000000..0dd6b1492 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/StandardLauncher.java @@ -0,0 +1,55 @@ +/****************************************************************************** + * Copyright (c) 2018 Red Hat Inc + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor; +import org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer; + +public class StandardLauncher implements Launcher { + private final ExecutorService execService; + private final T remoteProxy; + private final RemoteEndpoint remoteEndpoint; + private final ConcurrentMessageProcessor msgProcessor; + + public StandardLauncher(StreamMessageProducer reader, MessageConsumer messageConsumer, + ExecutorService execService, T remoteProxy, RemoteEndpoint remoteEndpoint) { + this(execService, remoteProxy, remoteEndpoint, + new ConcurrentMessageProcessor(reader, messageConsumer)); + } + + public StandardLauncher( + ExecutorService execService2, T remoteProxy2, RemoteEndpoint remoteEndpoint2, + ConcurrentMessageProcessor msgProcessor) { + this.execService = execService2; + this.remoteProxy = remoteProxy2; + this.remoteEndpoint = remoteEndpoint2; + this.msgProcessor = msgProcessor; + } + + @Override + public Future startListening() { + return msgProcessor.beginProcessing(execService); + } + + @Override + public T getRemoteProxy() { + return remoteProxy; + } + + @Override + public RemoteEndpoint getRemoteEndpoint() { + return remoteEndpoint; + } +} \ No newline at end of file diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/TracingMessageConsumer.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/TracingMessageConsumer.java new file mode 100644 index 000000000..78b015cd5 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/TracingMessageConsumer.java @@ -0,0 +1,221 @@ +/****************************************************************************** + * Copyright (c) 2016-2019 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc; + +import com.itsaky.androidide.utils.ILogger; + +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.json.StreamMessageConsumer; +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.NotificationMessage; +import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; + +import java.io.PrintWriter; +import java.time.Clock; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +/** + * A {@link MessageConsumer} that outputs logs in a format that can be parsed by the LSP Inspector. + * https://microsoft.github.io/language-server-protocol/inspector/ + */ +public class TracingMessageConsumer implements MessageConsumer { + private static final ILogger LOG = ILogger.newInstance("TracingMessageConsumer"); + + private final MessageConsumer messageConsumer; + private final Map sentRequests; + private final Map receivedRequests; + private final PrintWriter printWriter; + private final Clock clock; + private final DateTimeFormatter dateTimeFormatter; + + /** + * @param messageConsumer The {@link MessageConsumer} to wrap. + * @param sentRequests A map that keeps track of pending sent request data. + * @param receivedRequests A map that keeps track of pending received request data. + * @param printWriter Where to write the log to. + * @param clock The clock that is used to calculate timestamps and durations. + */ + public TracingMessageConsumer( + MessageConsumer messageConsumer, + Map sentRequests, + Map receivedRequests, + PrintWriter printWriter, + Clock clock) { + this(messageConsumer, sentRequests, receivedRequests, printWriter, clock, null); + } + + /** + * @param messageConsumer The {@link MessageConsumer} to wrap. + * @param sentRequests A map that keeps track of pending sent request data. + * @param receivedRequests A map that keeps track of pending received request data. + * @param printWriter Where to write the log to. + * @param clock The clock that is used to calculate timestamps and durations. + * @param locale THe Locale to format the timestamps and durations, or null to use + * default locale. + */ + public TracingMessageConsumer( + MessageConsumer messageConsumer, + Map sentRequests, + Map receivedRequests, + PrintWriter printWriter, + Clock clock, + Locale locale) { + this.messageConsumer = Objects.requireNonNull(messageConsumer); + this.sentRequests = Objects.requireNonNull(sentRequests); + this.receivedRequests = Objects.requireNonNull(receivedRequests); + this.printWriter = Objects.requireNonNull(printWriter); + this.clock = Objects.requireNonNull(clock); + if (locale == null) { + this.dateTimeFormatter = DateTimeFormatter.ofPattern("KK:mm:ss a").withZone(clock.getZone()); + } else { + this.dateTimeFormatter = + DateTimeFormatter.ofPattern("KK:mm:ss a", locale).withZone(clock.getZone()); + } + } + + /** + * Constructs a log string for a given {@link Message}. The type of the {@link MessageConsumer} + * determines if we're sending or receiving a message. The type of the @{link Message} determines + * if it is a request, response, or notification. + */ + @Override + public void consume(Message message) throws MessageIssueException, JsonRpcException { + final Instant now = clock.instant(); + final String date = dateTimeFormatter.format(now); + final String logString; + + if (messageConsumer instanceof StreamMessageConsumer) { + logString = consumeMessageSending(message, now, date); + } else if (messageConsumer instanceof RemoteEndpoint) { + logString = consumeMessageReceiving(message, now, date); + } else { + LOG.warn(String.format("Unknown MessageConsumer type: %s", messageConsumer)); + logString = null; + } + + if (logString != null) { + printWriter.print(logString); + printWriter.flush(); + } + + messageConsumer.consume(message); + } + + private String consumeMessageSending(Message message, Instant now, String date) { + if (message instanceof RequestMessage) { + RequestMessage requestMessage = (RequestMessage) message; + String id = requestMessage.getId(); + String method = requestMessage.getMethod(); + RequestMetadata requestMetadata = new RequestMetadata(method, now); + sentRequests.put(id, requestMetadata); + Object params = requestMessage.getParams(); + String paramsJson = MessageJsonHandler.toString(params); + String format = "[Trace - %s] Sending request '%s - (%s)'\nParams: %s\n\n\n"; + return String.format(format, date, method, id, paramsJson); + } else if (message instanceof ResponseMessage) { + ResponseMessage responseMessage = (ResponseMessage) message; + String id = responseMessage.getId(); + RequestMetadata requestMetadata = receivedRequests.remove(id); + if (requestMetadata == null) { + LOG.warn(String.format("Unmatched response message: %s", message)); + return null; + } + String method = requestMetadata.method; + long latencyMillis = now.toEpochMilli() - requestMetadata.start.toEpochMilli(); + Object result = responseMessage.getResult(); + String resultJson = MessageJsonHandler.toString(result); + String format = + "[Trace - %s] Sending response '%s - (%s)'. Processing request took %sms\nResult: %s\n\n\n"; + return String.format(format, date, method, id, latencyMillis, resultJson); + } else if (message instanceof NotificationMessage) { + NotificationMessage notificationMessage = (NotificationMessage) message; + String method = notificationMessage.getMethod(); + Object params = notificationMessage.getParams(); + String paramsJson = MessageJsonHandler.toString(params); + String format = "[Trace - %s] Sending notification '%s'\nParams: %s\n\n\n"; + return String.format(format, date, method, paramsJson); + } else { + LOG.warn(String.format("Unknown message type: %s", message)); + return null; + } + } + + private String consumeMessageReceiving(Message message, Instant now, String date) { + if (message instanceof RequestMessage) { + RequestMessage requestMessage = (RequestMessage) message; + String method = requestMessage.getMethod(); + String id = requestMessage.getId(); + RequestMetadata requestMetadata = new RequestMetadata(method, now); + receivedRequests.put(id, requestMetadata); + Object params = requestMessage.getParams(); + String paramsJson = MessageJsonHandler.toString(params); + String format = "[Trace - %s] Received request '%s - (%s)'\nParams: %s\n\n\n"; + return String.format(format, date, method, id, paramsJson); + } else if (message instanceof ResponseMessage) { + ResponseMessage responseMessage = (ResponseMessage) message; + String id = responseMessage.getId(); + RequestMetadata requestMetadata = sentRequests.remove(id); + if (requestMetadata == null) { + LOG.warn(String.format("Unmatched response message: %s", message)); + return null; + } + String method = requestMetadata.method; + long latencyMillis = now.toEpochMilli() - requestMetadata.start.toEpochMilli(); + Object result = responseMessage.getResult(); + String resultJson = MessageJsonHandler.toString(result); + Object error = responseMessage.getError(); + String errorJson = MessageJsonHandler.toString(error); + String format = + "[Trace - %s] Received response '%s - (%s)' in %sms\nResult: %s\nError: %s\n\n\n"; + return String.format(format, date, method, id, latencyMillis, resultJson, errorJson); + } else if (message instanceof NotificationMessage) { + NotificationMessage notificationMessage = (NotificationMessage) message; + String method = notificationMessage.getMethod(); + Object params = notificationMessage.getParams(); + String paramsJson = MessageJsonHandler.toString(params); + String format = "[Trace - %s] Received notification '%s'\nParams: %s\n\n\n"; + return String.format(format, date, method, paramsJson); + } else { + LOG.warn(String.format("Unknown message type: %s", message)); + return null; + } + } + + /** Data class for holding pending request metadata. */ + public static class RequestMetadata { + final String method; + final Instant start; + + public RequestMetadata(String method, Instant start) { + this.method = method; + this.start = start; + } + + @Override + public int hashCode() { + return Objects.hash(method, start); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RequestMetadata that = (RequestMetadata) o; + return Objects.equals(method, that.method) && Objects.equals(start, that.start); + } + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/ConcurrentMessageProcessor.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/ConcurrentMessageProcessor.java new file mode 100644 index 000000000..2457a714e --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/ConcurrentMessageProcessor.java @@ -0,0 +1,138 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +import com.itsaky.androidide.utils.ILogger; + +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.eclipse.lsp4j.jsonrpc.MessageProducer; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * This class connects a message producer with a message consumer by listening for new messages in a + * dedicated thread. + */ +public class ConcurrentMessageProcessor implements Runnable { + + private static final ILogger LOG = ILogger.newInstance("ConcurrentMessageProducer"); + private final MessageProducer messageProducer; + private final MessageConsumer messageConsumer; + private boolean isRunning; + + public ConcurrentMessageProcessor( + MessageProducer messageProducer, MessageConsumer messageConsumer) { + this.messageProducer = messageProducer; + this.messageConsumer = messageConsumer; + } + + /** + * Start a thread that listens for messages in the message producer and forwards them to the + * message consumer. + * + * @param messageProducer - produces messages, e.g. by reading from an input channel + * @param messageConsumer - processes messages and potentially forwards them to other consumers + * @param executorService - the thread is started using this service + * @return a future that is resolved when the started thread is terminated, e.g. by closing a + * stream + * @deprecated Please use the non-static ConcurrentMessageProcessor.beginProcessing() instead. + */ + @Deprecated + public static Future startProcessing( + MessageProducer messageProducer, + MessageConsumer messageConsumer, + ExecutorService executorService) { + ConcurrentMessageProcessor reader = + new ConcurrentMessageProcessor(messageProducer, messageConsumer); + final Future result = executorService.submit(reader); + return wrapFuture(result, messageProducer); + } + + public static Future wrapFuture(Future result, MessageProducer messageProducer) { + return new Future() { + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (mayInterruptIfRunning && messageProducer instanceof Closeable) { + try { + ((Closeable) messageProducer).close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return result.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return result.isCancelled(); + } + + @Override + public boolean isDone() { + return result.isDone(); + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + return (Void) result.get(); + } + + @Override + public Void get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return (Void) result.get(timeout, unit); + } + }; + } + + /** + * Start a thread that listens for messages in the message producer and forwards them to the + * message consumer. + * + * @param executorService - the thread is started using this service + * @return a future that is resolved when the started thread is terminated, e.g. by closing a + * stream + */ + public Future beginProcessing(ExecutorService executorService) { + final Future result = executorService.submit(this); + return wrapFuture(result, messageProducer); + } + + public void run() { + processingStarted(); + try { + messageProducer.listen(messageConsumer); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + processingEnded(); + } + } + + protected void processingStarted() { + if (isRunning) { + throw new IllegalStateException("The message processor is already running."); + } + isRunning = true; + } + + protected void processingEnded() { + isRunning = false; + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/JsonRpcMethod.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/JsonRpcMethod.java new file mode 100644 index 000000000..b2dcf13d8 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/JsonRpcMethod.java @@ -0,0 +1,88 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +import java.lang.reflect.Type; + +import com.google.gson.TypeAdapterFactory; + +/** + * A description of a JSON-RPC method. + */ +public class JsonRpcMethod { + + private final String methodName; + private final Type[] parameterTypes; + private final Type returnType; + private final TypeAdapterFactory returnTypeAdapterFactory; + private final boolean isNotification; + + private JsonRpcMethod(String methodName, Type[] parameterTypes, Type returnType, TypeAdapterFactory returnTypeAdapterFactory, + boolean isNotification) { + if (methodName == null) + throw new NullPointerException("methodName"); + this.methodName = methodName; + this.parameterTypes = parameterTypes; + this.returnType = returnType; + this.returnTypeAdapterFactory = returnTypeAdapterFactory; + this.isNotification = isNotification; + } + + public String getMethodName() { + return methodName; + } + + public Type[] getParameterTypes() { + return parameterTypes; + } + + public Type getReturnType() { + return returnType; + } + + public TypeAdapterFactory getReturnTypeAdapterFactory() { + return returnTypeAdapterFactory; + } + + public boolean isNotification() { + return isNotification; + } + + public static JsonRpcMethod notification(String name, Type... parameterTypes) { + return new JsonRpcMethod(name, parameterTypes, Void.class, null, true); + } + + public static JsonRpcMethod request(String name, Type returnType, Type... parameterTypes) { + return new JsonRpcMethod(name, parameterTypes, returnType, null, false); + } + + public static JsonRpcMethod request(String name, Type returnType, TypeAdapterFactory returnTypeAdapterFactory, Type... parameterTypes) { + return new JsonRpcMethod(name, parameterTypes, returnType, returnTypeAdapterFactory, false); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (isNotification) + builder.append("JsonRpcMethod (notification) {\n"); + else + builder.append("JsonRpcMethod (request) {\n"); + builder.append("\tmethodName: ").append(methodName).append('\n'); + if (parameterTypes != null) + builder.append("\tparameterTypes: ").append(parameterTypes).append('\n'); + if (returnType != null) + builder.append("\treturnType: ").append(returnType).append('\n'); + builder.append("}"); + return builder.toString(); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/JsonRpcMethodProvider.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/JsonRpcMethodProvider.java new file mode 100644 index 000000000..a7312003e --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/JsonRpcMethodProvider.java @@ -0,0 +1,26 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +import java.util.Map; + +import org.eclipse.lsp4j.jsonrpc.Endpoint; + +/** + * Provides {@link JsonRpcMethod}. Can be implemented by {@link Endpoint}s to + * provide information about the supported methods. + */ +public interface JsonRpcMethodProvider { + + Map supportedMethods(); + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MessageConstants.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MessageConstants.java new file mode 100644 index 000000000..e6676fa1a --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MessageConstants.java @@ -0,0 +1,22 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +public interface MessageConstants { + + public static String JSONRPC_VERSION = "2.0"; + public static String CONTENT_LENGTH_HEADER = "Content-Length"; + public static String CONTENT_TYPE_HEADER = "Content-Type"; + public static String JSON_MIME_TYPE = "application/json"; + public static String CRLF = "\r\n"; + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MessageJsonHandler.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MessageJsonHandler.java new file mode 100644 index 000000000..f7584c810 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MessageJsonHandler.java @@ -0,0 +1,164 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; + +import org.eclipse.lsp4j.jsonrpc.MessageIssueException; +import org.eclipse.lsp4j.jsonrpc.json.adapters.CollectionTypeAdapter; +import org.eclipse.lsp4j.jsonrpc.json.adapters.EitherTypeAdapter; +import org.eclipse.lsp4j.jsonrpc.json.adapters.EnumTypeAdapter; +import org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter; +import org.eclipse.lsp4j.jsonrpc.json.adapters.ThrowableTypeAdapter; +import org.eclipse.lsp4j.jsonrpc.json.adapters.TupleTypeAdapters; +import org.eclipse.lsp4j.jsonrpc.messages.CancelParams; +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.MessageIssue; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonParseException; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.MalformedJsonException; + +/** + * A wrapper around Gson that includes configuration required for JSON-RPC messages. + */ +public class MessageJsonHandler { + + public static final JsonRpcMethod CANCEL_METHOD = JsonRpcMethod.notification("$/cancelRequest", CancelParams.class); + + private final Gson gson; + + private final Map supportedMethods; + + private MethodProvider methodProvider; + + /** + * @param supportedMethods - a map used to resolve RPC methods in {@link #getJsonRpcMethod(String)} + */ + public MessageJsonHandler(Map supportedMethods) { + this.supportedMethods = supportedMethods; + this.gson = getDefaultGsonBuilder().create(); + } + + /** + * @param supportedMethods - a map used to resolve RPC methods in {@link #getJsonRpcMethod(String)} + * @param configureGson - a function that contributes to the GsonBuilder created by {@link #getDefaultGsonBuilder()} + */ + public MessageJsonHandler(Map supportedMethods, Consumer configureGson) { + this.supportedMethods = supportedMethods; + GsonBuilder gsonBuilder = getDefaultGsonBuilder(); + configureGson.accept(gsonBuilder); + this.gson = gsonBuilder.create(); + } + + /** + * Create a {@link GsonBuilder} with default settings for parsing JSON-RPC messages. + */ + public GsonBuilder getDefaultGsonBuilder() { + return new GsonBuilder() + .registerTypeAdapterFactory(new CollectionTypeAdapter.Factory()) + .registerTypeAdapterFactory(new ThrowableTypeAdapter.Factory()) + .registerTypeAdapterFactory(new EitherTypeAdapter.Factory()) + .registerTypeAdapterFactory(new TupleTypeAdapters.TwoTypeAdapterFactory()) + .registerTypeAdapterFactory(new EnumTypeAdapter.Factory()) + .registerTypeAdapterFactory(new MessageTypeAdapter.Factory(this)); + } + + public Gson getGson() { + return gson; + } + + /** + * Resolve an RPC method by name. + */ + public JsonRpcMethod getJsonRpcMethod(String name) { + JsonRpcMethod result = supportedMethods.get(name); + if (result != null) + return result; + else if (CANCEL_METHOD.getMethodName().equals(name)) + return CANCEL_METHOD; + return null; + } + + public MethodProvider getMethodProvider() { + return methodProvider; + } + + public void setMethodProvider(MethodProvider methodProvider) { + this.methodProvider = methodProvider; + } + + public Message parseMessage(CharSequence input) throws JsonParseException { + StringReader reader = new StringReader(input.toString()); + return parseMessage(reader); + } + + public Message parseMessage(Reader input) throws JsonParseException { + JsonReader jsonReader = new JsonReader(input); + Message message = gson.fromJson(jsonReader, Message.class); + + if (message != null) { + // Check whether the input has been fully consumed + try { + if (jsonReader.peek() != JsonToken.END_DOCUMENT) { + MessageIssue issue = new MessageIssue("JSON document was not fully consumed.", ResponseErrorCode.ParseError.getValue()); + throw new MessageIssueException(message, issue); + } + } catch (MalformedJsonException e) { + MessageIssue issue = new MessageIssue("Message could not be parsed.", ResponseErrorCode.ParseError.getValue(), e); + throw new MessageIssueException(message, issue); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + return message; + } + + public String serialize(Message message) { + StringWriter writer = new StringWriter(); + serialize(message, writer); + return writer.toString(); + } + + public void serialize(Message message, Writer output) throws JsonIOException { + gson.toJson(message, Message.class, output); + } + + + private static MessageJsonHandler toStringInstance; + + /** + * Perform JSON serialization of the given object using the default configuration of JSON-RPC messages + * enhanced with the pretty printing option. + */ + public static String toString(Object object) { + if (toStringInstance == null) { + toStringInstance = new MessageJsonHandler(Collections.emptyMap(), gsonBuilder -> { + gsonBuilder.setPrettyPrinting(); + }); + } + return toStringInstance.gson.toJson(object); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MethodProvider.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MethodProvider.java new file mode 100644 index 000000000..3ece1f93c --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/MethodProvider.java @@ -0,0 +1,23 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +public interface MethodProvider { + + /** + * Returns the method name for a given request id, or null if such request id is unknown. + * + * @return method name or {@code null} + */ + String resolveMethod(String requestId); + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/ResponseJsonAdapter.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/ResponseJsonAdapter.java new file mode 100644 index 000000000..3fe62d0c4 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/ResponseJsonAdapter.java @@ -0,0 +1,30 @@ +/****************************************************************************** + * Copyright (c) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.google.gson.TypeAdapterFactory; + +/** + * An annotation that indicates the Gson {@link TypeAdapterFactory} to use with a request response. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface ResponseJsonAdapter { + + Class value(); + +} \ No newline at end of file diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageConsumer.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageConsumer.java new file mode 100644 index 000000000..313c47233 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageConsumer.java @@ -0,0 +1,98 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.eclipse.lsp4j.jsonrpc.JsonRpcException; +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.eclipse.lsp4j.jsonrpc.messages.Message; + +/** + * A message consumer that serializes messages to JSON and sends them to an output stream. + */ +public class StreamMessageConsumer implements MessageConsumer, MessageConstants { + + private final String encoding; + private final MessageJsonHandler jsonHandler; + + private final Object outputLock = new Object(); + + private OutputStream output; + + public StreamMessageConsumer(MessageJsonHandler jsonHandler) { + this(null, StandardCharsets.UTF_8.name(), jsonHandler); + } + + public StreamMessageConsumer(OutputStream output, MessageJsonHandler jsonHandler) { + this(output, StandardCharsets.UTF_8.name(), jsonHandler); + } + + public StreamMessageConsumer(OutputStream output, String encoding, MessageJsonHandler jsonHandler) { + this.output = output; + this.encoding = encoding; + this.jsonHandler = jsonHandler; + } + + public OutputStream getOutput() { + return output; + } + + public void setOutput(OutputStream output) { + this.output = output; + } + + @Override + public void consume(Message message) { + try { + String content = jsonHandler.serialize(message); + byte[] contentBytes = content.getBytes(encoding); + int contentLength = contentBytes.length; + + String header = getHeader(contentLength); + byte[] headerBytes = header.getBytes(StandardCharsets.US_ASCII); + + synchronized (outputLock) { + output.write(headerBytes); + output.write(contentBytes); + output.flush(); + } + } catch (IOException exception) { + throw new JsonRpcException(exception); + } + } + + /** + * Construct a header to be prepended to the actual content. This implementation writes + * {@code Content-Length} and {@code Content-Type} attributes according to the LSP specification. + */ + protected String getHeader(int contentLength) { + StringBuilder headerBuilder = new StringBuilder(); + appendHeader(headerBuilder, CONTENT_LENGTH_HEADER, contentLength).append(CRLF); + if (!StandardCharsets.UTF_8.name().equals(encoding)) { + appendHeader(headerBuilder, CONTENT_TYPE_HEADER, JSON_MIME_TYPE); + headerBuilder.append("; charset=").append(encoding).append(CRLF); + } + headerBuilder.append(CRLF); + return headerBuilder.toString(); + } + + /** + * Append a header attribute to the given builder. + */ + protected StringBuilder appendHeader(StringBuilder builder, String name, Object value) { + return builder.append(name).append(": ").append(value); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageProducer.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageProducer.java new file mode 100644 index 000000000..c57b9199f --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageProducer.java @@ -0,0 +1,212 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json; + +import com.itsaky.androidide.utils.ILogger; + +import org.eclipse.lsp4j.jsonrpc.JsonRpcException; +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.eclipse.lsp4j.jsonrpc.MessageIssueException; +import org.eclipse.lsp4j.jsonrpc.MessageIssueHandler; +import org.eclipse.lsp4j.jsonrpc.MessageProducer; +import org.eclipse.lsp4j.jsonrpc.messages.Message; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** A message producer that reads from an input stream and parses messages from JSON. */ +public class StreamMessageProducer implements MessageProducer, Closeable, MessageConstants { + + private static final ILogger LOG = ILogger.newInstance("StreamMessageProducer"); + + private final MessageJsonHandler jsonHandler; + private final MessageIssueHandler issueHandler; + + private InputStream input; + + private MessageConsumer callback; + private boolean keepRunning; + + public StreamMessageProducer(InputStream input, MessageJsonHandler jsonHandler) { + this(input, jsonHandler, null); + } + + public StreamMessageProducer( + InputStream input, MessageJsonHandler jsonHandler, MessageIssueHandler issueHandler) { + this.input = input; + this.jsonHandler = jsonHandler; + this.issueHandler = issueHandler; + } + + public InputStream getInput() { + return input; + } + + public void setInput(InputStream input) { + this.input = input; + } + + @Override + public void listen(MessageConsumer callback) { + if (keepRunning) { + throw new IllegalStateException("This StreamMessageProducer is already running."); + } + this.keepRunning = true; + this.callback = callback; + try { + StringBuilder headerBuilder = null; + StringBuilder debugBuilder = null; + boolean newLine = false; + Headers headers = new Headers(); + while (keepRunning) { + int c = input.read(); + if (c == -1) { + // End of input stream has been reached + keepRunning = false; + } else { + if (debugBuilder == null) debugBuilder = new StringBuilder(); + debugBuilder.append((char) c); + if (c == '\n') { + if (newLine) { + // Two consecutive newlines have been read, which signals the start of the message + // content + if (headers.contentLength < 0) { + fireError( + new IllegalStateException( + "Missing header " + + CONTENT_LENGTH_HEADER + + " in input \"" + + debugBuilder + + "\"")); + } else { + boolean result = handleMessage(input, headers); + if (!result) keepRunning = false; + newLine = false; + } + headers = new Headers(); + debugBuilder = null; + } else if (headerBuilder != null) { + // A single newline ends a header line + parseHeader(headerBuilder.toString(), headers); + headerBuilder = null; + } + newLine = true; + } else if (c != '\r') { + // Add the input to the current header line + if (headerBuilder == null) headerBuilder = new StringBuilder(); + headerBuilder.append((char) c); + newLine = false; + } + } + } // while (keepRunning) + } catch (IOException exception) { + if (JsonRpcException.indicatesStreamClosed(exception)) { + // Only log the error if we had intended to keep running + if (keepRunning) fireStreamClosed(exception); + } else throw new JsonRpcException(exception); + } finally { + this.callback = null; + this.keepRunning = false; + } + } + + /** Log an error. */ + protected void fireError(Throwable error) { + String message = + error.getMessage() != null + ? error.getMessage() + : "An error occurred while processing an incoming message."; + LOG.error(message, error); + } + + /** + * Read the JSON content part of a message, parse it, and notify the callback. + * + * @return {@code true} if we should continue reading from the input stream, {@code false} if we + * should stop + */ + protected boolean handleMessage(InputStream input, Headers headers) throws IOException { + if (callback == null) callback = message -> LOG.info("Received message: " + message); + + try { + int contentLength = headers.contentLength; + byte[] buffer = new byte[contentLength]; + int bytesRead = 0; + + while (bytesRead < contentLength) { + int readResult = input.read(buffer, bytesRead, contentLength - bytesRead); + if (readResult == -1) return false; + bytesRead += readResult; + } + + String content = new String(buffer, headers.charset); + try { + Message message = jsonHandler.parseMessage(content); + callback.consume(message); + } catch (MessageIssueException exception) { + // An issue was found while parsing or validating the message + if (issueHandler != null) + issueHandler.handle(exception.getRpcMessage(), exception.getIssues()); + else fireError(exception); + } + } catch (Exception exception) { + // UnsupportedEncodingException can be thrown by String constructor + // JsonParseException can be thrown by jsonHandler + // We also catch arbitrary exceptions that are thrown by message consumers in order to keep + // this thread alive + fireError(exception); + } + return true; + } + + /** Parse a header attribute and set the corresponding data in the {@link Headers} fields. */ + protected void parseHeader(String line, Headers headers) { + int sepIndex = line.indexOf(':'); + if (sepIndex >= 0) { + String key = line.substring(0, sepIndex).trim(); + switch (key) { + case CONTENT_LENGTH_HEADER: + try { + headers.contentLength = Integer.parseInt(line.substring(sepIndex + 1).trim()); + } catch (NumberFormatException e) { + fireError(e); + } + break; + case CONTENT_TYPE_HEADER: + { + int charsetIndex = line.indexOf("charset="); + if (charsetIndex >= 0) headers.charset = line.substring(charsetIndex + 8).trim(); + break; + } + } + } + } + + /** Report that the stream was closed through an exception. */ + protected void fireStreamClosed(Exception cause) { + String message = + cause.getMessage() != null ? cause.getMessage() : "The input stream was closed."; + LOG.info(message, cause); + } + + @Override + public void close() { + keepRunning = false; + } + + protected static class Headers { + public int contentLength = -1; + public String charset = StandardCharsets.UTF_8.name(); + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/CollectionTypeAdapter.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/CollectionTypeAdapter.java new file mode 100644 index 000000000..96d34f80a --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/CollectionTypeAdapter.java @@ -0,0 +1,146 @@ +/****************************************************************************** + * Copyright (c) 2016-2017 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.function.Supplier; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * A specialized type adapter for collections that can handle single values. + */ +public class CollectionTypeAdapter extends TypeAdapter> { + + public static class Factory implements TypeAdapterFactory { + + @Override + @SuppressWarnings({ "unchecked" }) + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (!Collection.class.isAssignableFrom(typeToken.getRawType())) + return null; + + Type[] elementTypes = TypeUtils.getElementTypes(typeToken, Collection.class); + if (elementTypes.length != 1) + return null; + Type elementType = elementTypes[0]; + TypeAdapter elementTypeAdapter; + if (elementType == Object.class) + elementTypeAdapter = new JsonElementTypeAdapter(gson); + else + elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType)); + Supplier> constructor = getConstructor((Class>) typeToken.getRawType()); + return (TypeAdapter) create(gson, elementType, elementTypeAdapter, constructor); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected TypeAdapter create(Gson gson, Type elementType, TypeAdapter elementTypeAdapter, Supplier> constructor) { + return new CollectionTypeAdapter(gson, elementType, elementTypeAdapter, constructor); + } + + protected Supplier> getConstructor(Class> rawType) { + try { + Constructor> constructor = rawType.getDeclaredConstructor(); + return () -> { + try { + return constructor.newInstance(); + } catch (Exception e) { + throw new JsonParseException(e); + } + }; + } catch (Exception e) { + if (SortedSet.class.isAssignableFrom(rawType)) + return () -> new TreeSet(); + else if (Set.class.isAssignableFrom(rawType)) + return () -> new LinkedHashSet(); + else if (Queue.class.isAssignableFrom(rawType)) + return () -> new LinkedList(); + else + return () -> new ArrayList(); + } + } + + } + + private final Gson gson; + private final Type elementType; + private final TypeAdapter elementTypeAdapter; + private final Supplier> constructor; + + public CollectionTypeAdapter(Gson gson, Type elementType, TypeAdapter elementTypeAdapter, Supplier> constructor) { + this.gson = gson; + this.elementType = elementType; + this.elementTypeAdapter = elementTypeAdapter; + this.constructor = constructor; + } + + @Override + public Collection read(JsonReader in) throws IOException { + JsonToken peek = in.peek(); + if (peek == JsonToken.NULL) { + in.nextNull(); + return null; + } else if (peek == JsonToken.BEGIN_ARRAY) { + Collection collection = constructor.get(); + in.beginArray(); + while (in.hasNext()) { + E instance = elementTypeAdapter.read(in); + collection.add(instance); + } + in.endArray(); + return collection; + } else { + Collection collection = constructor.get(); + E instance = elementTypeAdapter.read(in); + collection.add(instance); + return collection; + } + } + + @Override + public void write(JsonWriter out, Collection collection) throws IOException { + if (collection == null) { + out.nullValue(); + return; + } + out.beginArray(); + for (E element : collection) { + if (element != null && elementType != element.getClass() + && (elementType instanceof TypeVariable || elementType instanceof Class)) { + @SuppressWarnings("unchecked") + TypeAdapter runtimeTypeAdapter = (TypeAdapter) gson.getAdapter(TypeToken.get(element.getClass())); + runtimeTypeAdapter.write(out, element); + } else { + elementTypeAdapter.write(out, element); + } + } + out.endArray(); + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/CollectionTypeAdapterFactory.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/CollectionTypeAdapterFactory.java new file mode 100644 index 000000000..7a153e285 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/CollectionTypeAdapterFactory.java @@ -0,0 +1,40 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.function.Supplier; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; + +/** + * @deprecated Use {@link CollectionTypeAdapter.Factory} instead. + */ +@Deprecated +public class CollectionTypeAdapterFactory extends CollectionTypeAdapter.Factory { + + /** + * @deprecated Use {@link CollectionTypeAdapter} instead. + */ + @Deprecated + protected static class Adapter extends CollectionTypeAdapter { + + public Adapter(Gson gson, Type elementType, TypeAdapter elementTypeAdapter, + Supplier> constructor) { + super(gson, elementType, elementTypeAdapter, constructor); + } + + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EitherTypeAdapter.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EitherTypeAdapter.java new file mode 100644 index 000000000..707c50613 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EitherTypeAdapter.java @@ -0,0 +1,286 @@ +/****************************************************************************** + * Copyright (c) 2016-2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashSet; +import java.util.function.Predicate; + +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Either3; +import org.eclipse.lsp4j.jsonrpc.messages.Tuple; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Type adapter for {@link Either} and {@link Either3}. + */ +public class EitherTypeAdapter extends TypeAdapter> { + + public static class Factory implements TypeAdapterFactory { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (!TypeUtils.isEither(typeToken.getType())) { + return null; + } + return new EitherTypeAdapter(gson, typeToken); + } + + } + + /** + * A predicate that is usedful for checking alternatives in case both the left and the right type + * are JSON object types. + */ + public static class PropertyChecker implements Predicate { + + private final String propertyName; + private final String expectedValue; + private final Class expectedType; + + public PropertyChecker(String propertyName) { + this.propertyName = propertyName; + this.expectedValue = null; + this.expectedType = null; + } + + public PropertyChecker(String propertyName, String expectedValue) { + this.propertyName = propertyName; + this.expectedValue = expectedValue; + this.expectedType = null; + } + + public PropertyChecker(String propertyName, Class expectedType) { + this.propertyName = propertyName; + this.expectedType = expectedType; + this.expectedValue = null; + } + + @Override + public boolean test(JsonElement element) { + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + JsonElement value = object.get(propertyName); + if (expectedValue != null) + return value != null && value.isJsonPrimitive() && expectedValue.equals(value.getAsString()); + else if (expectedType != null) + return expectedType.isInstance(value); + else + return value != null; + } + return false; + } + + } + + /** + * A predicate for the case that a type alternative is a list. + */ + public static class ListChecker implements Predicate { + + private final Predicate elementChecker; + private final boolean resultIfEmpty; + + public ListChecker(Predicate elementChecker) { + this(elementChecker, false); + } + + public ListChecker(Predicate elementChecker, boolean resultIfEmpty) { + this.elementChecker = elementChecker; + this.resultIfEmpty = resultIfEmpty; + } + + @Override + public boolean test(JsonElement t) { + if (elementChecker.test(t)) + return true; + if (t.isJsonArray()) { + JsonArray array = t.getAsJsonArray(); + if (array.size() == 0) + return resultIfEmpty; + for (JsonElement e : array) { + if (elementChecker.test(e)) + return true; + } + } + return false; + } + + } + + protected final TypeToken> typeToken; + protected final EitherTypeArgument left; + protected final EitherTypeArgument right; + protected final Predicate leftChecker; + protected final Predicate rightChecker; + + public EitherTypeAdapter(Gson gson, TypeToken> typeToken) { + this(gson, typeToken, null, null); + } + + public EitherTypeAdapter(Gson gson, TypeToken> typeToken, Predicate leftChecker, Predicate rightChecker) { + this.typeToken = typeToken; + Type[] elementTypes = TypeUtils.getElementTypes(typeToken, Either.class); + this.left = new EitherTypeArgument(gson, elementTypes[0]); + this.right = new EitherTypeArgument(gson, elementTypes[1]); + this.leftChecker = leftChecker; + this.rightChecker = rightChecker; + } + + @Override + public void write(JsonWriter out, Either value) throws IOException { + if (value == null) { + out.nullValue(); + } else if (value.isLeft()) { + left.write(out, value.getLeft()); + } else { + right.write(out, value.getRight()); + } + } + + @Override + public Either read(JsonReader in) throws IOException { + JsonToken next = in.peek(); + if (next == JsonToken.NULL) { + in.nextNull(); + return null; + } + return create(next, in); + } + + protected Either create(JsonToken nextToken, JsonReader in) throws IOException { + boolean matchesLeft = left.isAssignable(nextToken); + boolean matchesRight = right.isAssignable(nextToken); + if (matchesLeft && matchesRight) { + if (leftChecker != null || rightChecker != null) { + JsonElement element = JsonParser.parseReader(in); + if (leftChecker != null && leftChecker.test(element)) + // Parse the left alternative from the JSON element tree + return createLeft(left.read(element)); + if (rightChecker != null && rightChecker.test(element)) + // Parse the right alternative from the JSON element tree + return createRight(right.read(element)); + } + throw new JsonParseException("Ambiguous Either type: token " + nextToken + " matches both alternatives."); + } else if (matchesLeft) { + // Parse the left alternative from the JSON stream + return createLeft(left.read(in)); + } else if (matchesRight) { + // Parse the right alternative from the JSON stream + return createRight(right.read(in)); + } else if (leftChecker != null || rightChecker != null) { + // If result is not the list but directly the only item in the list + JsonElement element = JsonParser.parseReader(in); + if (leftChecker != null && leftChecker.test(element)) + // Parse the left alternative from the JSON element tree + return createLeft(left.read(element)); + if (rightChecker != null && rightChecker.test(element)) + // Parse the right alternative from the JSON element tree + return createRight(right.read(element)); + } + throw new JsonParseException("Unexpected token " + nextToken + ": expected " + left + " | " + right + " tokens."); + } + + @SuppressWarnings("unchecked") + protected Either createLeft(L obj) throws IOException { + if (Either3.class.isAssignableFrom(typeToken.getRawType())) + return (Either) Either3.forLeft3(obj); + return Either.forLeft(obj); + } + + @SuppressWarnings("unchecked") + protected Either createRight(R obj) throws IOException { + if (Either3.class.isAssignableFrom(typeToken.getRawType())) + return (Either) Either3.forRight3((Either) obj); + return Either.forRight(obj); + } + + protected static class EitherTypeArgument { + + protected final TypeToken typeToken; + protected final TypeAdapter adapter; + protected final Collection expectedTokens; + + @SuppressWarnings("unchecked") + public EitherTypeArgument(Gson gson, Type type) { + this.typeToken = (TypeToken) TypeToken.get(type); + this.adapter = (type == Object.class) ? (TypeAdapter) new JsonElementTypeAdapter(gson) : gson.getAdapter(this.typeToken); + this.expectedTokens = new HashSet<>(); + for (Type expectedType : TypeUtils.getExpectedTypes(type)) { + Class rawType = TypeToken.get(expectedType).getRawType(); + JsonToken expectedToken = getExpectedToken(rawType); + expectedTokens.add(expectedToken); + } + } + + protected JsonToken getExpectedToken(Class rawType) { + if (rawType.isArray() || Collection.class.isAssignableFrom(rawType) || Tuple.class.isAssignableFrom(rawType)) { + return JsonToken.BEGIN_ARRAY; + } + if (Boolean.class.isAssignableFrom(rawType)) { + return JsonToken.BOOLEAN; + } + if (Number.class.isAssignableFrom(rawType) || Enum.class.isAssignableFrom(rawType)) { + return JsonToken.NUMBER; + } + if (Character.class.isAssignableFrom(rawType) || String.class.isAssignableFrom(rawType)) { + return JsonToken.STRING; + } + return JsonToken.BEGIN_OBJECT; + } + + public boolean isAssignable(JsonToken jsonToken) { + return this.expectedTokens.contains(jsonToken); + } + + public void write(JsonWriter out, T value) throws IOException { + this.adapter.write(out, value); + } + + public T read(JsonReader in) throws IOException { + return this.adapter.read(in); + } + + public T read(JsonElement element) throws IOException { + return this.adapter.fromJsonTree(element); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (JsonToken expectedToken : expectedTokens) { + if (builder.length() != 0) { + builder.append(" | "); + } + builder.append(expectedToken); + } + return builder.toString(); + } + + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EitherTypeAdapterFactory.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EitherTypeAdapterFactory.java new file mode 100644 index 000000000..5abbe8320 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EitherTypeAdapterFactory.java @@ -0,0 +1,37 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +/** + * @deprecated Use {@link EitherTypeAdapter.Factory} instead. + */ +@Deprecated +public class EitherTypeAdapterFactory extends EitherTypeAdapter.Factory { + + /** + * @deprecated Use {@link EitherTypeAdapter} instead. + */ + @Deprecated + protected static class Adapter extends EitherTypeAdapter { + + public Adapter(Gson gson, TypeToken> typeToken) { + super(gson, typeToken); + } + + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EnumTypeAdapter.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EnumTypeAdapter.java new file mode 100644 index 000000000..c24ded2f8 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EnumTypeAdapter.java @@ -0,0 +1,105 @@ +/****************************************************************************** + * Copyright (c) 2016-2017 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * A custom type adapter for enums that uses integer values. + */ +public class EnumTypeAdapter> extends TypeAdapter { + + public static class Factory implements TypeAdapterFactory { + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public TypeAdapter create(Gson gson, TypeToken typeToken) { + Class rawType = typeToken.getRawType(); + if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) + return null; + if (!rawType.isEnum()) + rawType = rawType.getSuperclass(); + try { + return new EnumTypeAdapter(rawType); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + } + + private static String VALUE_FIELD_NAME = "value"; + + private final Map nameToConstant = new HashMap<>(); + private final Map valueToConstant = new HashMap<>(); + private final Map constantToValue = new HashMap<>(); + + EnumTypeAdapter(Class classOfT) throws IllegalAccessException { + try { + Field valueField = classOfT.getDeclaredField(VALUE_FIELD_NAME); + if (valueField.getType() != int.class && valueField.getType() != Integer.class) + throw new IllegalArgumentException("The field 'value' must contain an integer value."); + valueField.setAccessible(true); + for (T constant : classOfT.getEnumConstants()) { + nameToConstant.put(constant.name(), constant); + Integer constValue = (Integer) valueField.get(constant); + valueToConstant.put(constValue, constant); + constantToValue.put(constant, constValue); + } + } catch (NoSuchFieldException e) { + for (T constant : classOfT.getEnumConstants()) { + nameToConstant.put(constant.name(), constant); + int constValue = constant.ordinal(); + valueToConstant.put(constValue, constant); + constantToValue.put(constant, constValue); + } + } + } + + @Override + public T read(JsonReader in) throws IOException { + JsonToken peek = in.peek(); + if (peek == JsonToken.NULL) { + in.nextNull(); + return null; + } else if (peek == JsonToken.NUMBER) { + return valueToConstant.get(in.nextInt()); + } else { + String string = in.nextString(); + try { + return valueToConstant.get(Integer.parseInt(string)); + } catch (NumberFormatException e) { + return nameToConstant.get(string); + } + } + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + if (value != null) + out.value(constantToValue.get(value)); + else + out.value((String) null); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EnumTypeAdapterFactory.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EnumTypeAdapterFactory.java new file mode 100644 index 000000000..ee23ddeb2 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/EnumTypeAdapterFactory.java @@ -0,0 +1,20 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +/** + * @deprecated Use {@link EnumTypeAdapter.Factory} instead. + */ +@Deprecated +public class EnumTypeAdapterFactory extends EnumTypeAdapter.Factory { + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/JsonElementTypeAdapter.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/JsonElementTypeAdapter.java new file mode 100644 index 000000000..0e19daa9a --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/JsonElementTypeAdapter.java @@ -0,0 +1,68 @@ +/****************************************************************************** + * Copyright (c) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.io.IOException; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * A type adapter that reads every input into a tree of {@link JsonElement}s. + */ +public class JsonElementTypeAdapter extends TypeAdapter { + + /** + * This factory should not be registered with a GsonBuilder because it always matches. + * Use it as argument to a {@link com.google.gson.annotations.JsonAdapter} annotation like this: + * {@code @JsonAdapter(JsonElementTypeAdapter.Factory.class)} + */ + public static class Factory implements TypeAdapterFactory { + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + return (TypeAdapter) new JsonElementTypeAdapter(gson); + } + + } + + private final Gson gson; + private final TypeAdapter adapter; + + public JsonElementTypeAdapter(Gson gson) { + this.gson = gson; + this.adapter = gson.getAdapter(JsonElement.class); + } + + @Override + public JsonElement read(JsonReader in) throws IOException { + return adapter.read(in); + } + + @Override + public void write(JsonWriter out, Object value) throws IOException { + if (value == null) { + out.nullValue(); + } else if (value instanceof JsonElement) { + adapter.write(out, (JsonElement) value); + } else { + gson.toJson(value, value.getClass(), out);; + } + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/MessageTypeAdapter.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/MessageTypeAdapter.java new file mode 100644 index 000000000..bffdecb1e --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/MessageTypeAdapter.java @@ -0,0 +1,459 @@ +/****************************************************************************** + * Copyright (c) 2016-2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.io.EOFException; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.eclipse.lsp4j.jsonrpc.MessageIssueException; +import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod; +import org.eclipse.lsp4j.jsonrpc.json.MessageConstants; +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.json.MethodProvider; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.MessageIssue; +import org.eclipse.lsp4j.jsonrpc.messages.NotificationMessage; +import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonNull; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.MalformedJsonException; + +/** + * The type adapter for messages dispatches between the different message types: {@link RequestMessage}, + * {@link ResponseMessage}, and {@link NotificationMessage}. + */ +public class MessageTypeAdapter extends TypeAdapter { + + public static class Factory implements TypeAdapterFactory { + + private final MessageJsonHandler handler; + + public Factory(MessageJsonHandler handler) { + this.handler = handler; + } + + @Override + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (!Message.class.isAssignableFrom(typeToken.getRawType())) + return null; + return (TypeAdapter) new MessageTypeAdapter(handler, gson); + } + + } + + private static Type[] EMPTY_TYPE_ARRAY = {}; + + private final MessageJsonHandler handler; + private final Gson gson; + + public MessageTypeAdapter(MessageJsonHandler handler, Gson gson) { + this.handler = handler; + this.gson = gson; + } + + @Override + public Message read(JsonReader in) throws IOException, JsonIOException, JsonSyntaxException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + in.beginObject(); + String jsonrpc = null, method = null; + Either id = null; + Object rawParams = null; + Object rawResult = null; + ResponseError responseError = null; + try { + + while (in.hasNext()) { + String name = in.nextName(); + switch (name) { + case "jsonrpc": { + jsonrpc = in.nextString(); + break; + } + case "id": { + if (in.peek() == JsonToken.NUMBER) + id = Either.forRight(in.nextInt()); + else + id = Either.forLeft(in.nextString()); + break; + } + case "method": { + method = in.nextString(); + break; + } + case "params": { + rawParams = parseParams(in, method); + break; + } + case "result": { + rawResult = parseResult(in, id != null ? id.get().toString() : null); + break; + } + case "error": { + responseError = gson.fromJson(in, ResponseError.class); + break; + } + default: + in.skipValue(); + } + } + Object params = parseParams(rawParams, method); + Object result = parseResult(rawResult, id != null ? id.get().toString() : null); + + in.endObject(); + return createMessage(jsonrpc, id, method, params, result, responseError); + + } catch (JsonSyntaxException | MalformedJsonException | EOFException exception) { + if (id != null || method != null) { + // Create a message and bundle it to an exception with an issue that wraps the original exception + Message message = createMessage(jsonrpc, id, method, rawParams, rawResult, responseError); + MessageIssue issue = new MessageIssue("Message could not be parsed.", ResponseErrorCode.ParseError.getValue(), exception); + throw new MessageIssueException(message, issue); + } else { + throw exception; + } + } + } + + /** + * Convert the json input into the result object corresponding to the call made + * by id. + * + * If the id is not known until after parsing, call + * {@link #parseResult(Object, String)} on the return value of this call for a + * second chance conversion. + * + * @param in + * json input to read from + * @param id + * id of request message this is in response to + * @return correctly typed object if the correct expected type can be + * determined, or a JsonElement representing the result + */ + protected Object parseResult(JsonReader in, String id) throws JsonIOException, JsonSyntaxException { + Type type = null; + MethodProvider methodProvider = handler.getMethodProvider(); + if (methodProvider != null && id != null) { + String resolvedMethod = methodProvider.resolveMethod(id); + if (resolvedMethod != null) { + JsonRpcMethod jsonRpcMethod = handler.getJsonRpcMethod(resolvedMethod); + if (jsonRpcMethod != null) { + type = jsonRpcMethod.getReturnType(); + if (jsonRpcMethod.getReturnTypeAdapterFactory() != null) { + TypeAdapter typeAdapter = jsonRpcMethod.getReturnTypeAdapterFactory().create(gson, TypeToken.get(type)); + try { + if (typeAdapter != null) + return typeAdapter.read(in); + } catch (IOException exception) { + throw new JsonIOException(exception); + } + } + } + } + } + return fromJson(in, type); + } + + /** + * Convert the JsonElement into the result object corresponding to the call made + * by id. If the result is already converted, does nothing. + * + * @param result + * json element to read from + * @param id + * id of request message this is in response to + * @return correctly typed object if the correct expected type can be + * determined, or result unmodified if no conversion can be done. + */ + protected Object parseResult(Object result, String id) throws JsonSyntaxException { + if (result instanceof JsonElement) { + // Type of result could not be resolved - try again with the parsed JSON tree + Type type = null; + MethodProvider methodProvider = handler.getMethodProvider(); + if (methodProvider != null) { + String resolvedMethod = methodProvider.resolveMethod(id); + if (resolvedMethod != null) { + JsonRpcMethod jsonRpcMethod = handler.getJsonRpcMethod(resolvedMethod); + if (jsonRpcMethod != null) { + type = jsonRpcMethod.getReturnType(); + if (jsonRpcMethod.getReturnTypeAdapterFactory() != null) { + TypeAdapter typeAdapter = jsonRpcMethod.getReturnTypeAdapterFactory().create(gson, TypeToken.get(type)); + if (typeAdapter != null) + return typeAdapter.fromJsonTree((JsonElement) result); + } + } + } + } + return fromJson((JsonElement) result, type); + } + return result; + } + + /** + * Convert the json input into the parameters object corresponding to the call + * made by method. + * + * If the method is not known until after parsing, call + * {@link #parseParams(Object, String)} on the return value of this call for a + * second chance conversion. + * + * @param in + * json input to read from + * @param method + * method name of request + * @return correctly typed object if the correct expected type can be + * determined, or a JsonElement representing the parameters + */ + protected Object parseParams(JsonReader in, String method) throws IOException, JsonIOException { + JsonToken next = in.peek(); + if (next == JsonToken.NULL) { + in.nextNull(); + return null; + } + Type[] parameterTypes = getParameterTypes(method); + if (parameterTypes.length == 1) { + return fromJson(in, parameterTypes[0]); + } + if (parameterTypes.length > 1 && next == JsonToken.BEGIN_ARRAY) { + List parameters = new ArrayList(parameterTypes.length); + int index = 0; + in.beginArray(); + while (in.hasNext()) { + Type parameterType = index < parameterTypes.length ? parameterTypes[index] : null; + Object parameter = fromJson(in, parameterType); + parameters.add(parameter); + index++; + } + in.endArray(); + while (index < parameterTypes.length) { + parameters.add(null); + index++; + } + return parameters; + } + JsonElement rawParams = JsonParser.parseReader(in); + if (method != null && parameterTypes.length == 0 && ( + rawParams.isJsonArray() && rawParams.getAsJsonArray().size() == 0 + || rawParams.isJsonObject() && rawParams.getAsJsonObject().size() == 0)) { + return null; + } + return rawParams; + } + + /** + * Convert the JsonElement into the parameters object corresponding to the call made + * by method. If the result is already converted, does nothing. + * + * @param params + * json element to read from + * @param method + * method name of request + * @return correctly typed object if the correct expected type can be + * determined, or params unmodified if no conversion can be done. + */ + protected Object parseParams(Object params, String method) { + if (isNull(params)) { + return null; + } + if (!(params instanceof JsonElement)) { + return params; + } + JsonElement rawParams = (JsonElement) params; + Type[] parameterTypes = getParameterTypes(method); + if (parameterTypes.length == 1) { + return fromJson(rawParams, parameterTypes[0]); + } + if (parameterTypes.length > 1 && rawParams instanceof JsonArray) { + JsonArray array = (JsonArray) rawParams; + List parameters = new ArrayList(Math.max(array.size(), parameterTypes.length)); + int index = 0; + Iterator iterator = array.iterator(); + while (iterator.hasNext()) { + Type parameterType = index < parameterTypes.length ? parameterTypes[index] : null; + Object parameter = fromJson(iterator.next(), parameterType); + parameters.add(parameter); + index++; + } + while (index < parameterTypes.length) { + parameters.add(null); + index++; + } + return parameters; + } + if (method != null && parameterTypes.length == 0 && ( + rawParams.isJsonArray() && rawParams.getAsJsonArray().size() == 0 + || rawParams.isJsonObject() && rawParams.getAsJsonObject().size() == 0)) { + return null; + } + return rawParams; + } + + protected Object fromJson(JsonReader in, Type type) throws JsonIOException { + if (isNullOrVoidType(type)) { + return JsonParser.parseReader(in); + } + return gson.fromJson(in, type); + } + + protected Object fromJson(JsonElement element, Type type) { + if (isNull(element)) { + return null; + } + if (isNullOrVoidType(type)) { + return element; + } + Object value = gson.fromJson(element, type); + if (isNull(value)) { + return null; + } + return value; + } + + protected boolean isNull(Object value) { + return value == null || value instanceof JsonNull; + } + + protected boolean isNullOrVoidType(Type type) { + return type == null || Void.class == type; + } + + protected Type[] getParameterTypes(String method) { + if (method != null) { + JsonRpcMethod jsonRpcMethod = handler.getJsonRpcMethod(method); + if (jsonRpcMethod != null) + return jsonRpcMethod.getParameterTypes(); + } + return EMPTY_TYPE_ARRAY; + } + + protected Message createMessage(String jsonrpc, Either id, String method, Object params, + Object responseResult, ResponseError responseError) throws JsonParseException { + if (id != null && method != null) { + RequestMessage message = new RequestMessage(); + message.setJsonrpc(jsonrpc); + message.setRawId(id); + message.setMethod(method); + message.setParams(params); + return message; + } else if (id != null) { + ResponseMessage message = new ResponseMessage(); + message.setJsonrpc(jsonrpc); + message.setRawId(id); + if (responseError != null) + message.setError(responseError); + else + message.setResult(responseResult); + return message; + } else if (method != null) { + NotificationMessage message = new NotificationMessage(); + message.setJsonrpc(jsonrpc); + message.setMethod(method); + message.setParams(params); + return message; + } else { + throw new JsonParseException("Unable to identify the input message."); + } + } + + @Override + public void write(JsonWriter out, Message message) throws IOException { + out.beginObject(); + out.name("jsonrpc"); + out.value(message.getJsonrpc() == null ? MessageConstants.JSONRPC_VERSION : message.getJsonrpc()); + + if (message instanceof RequestMessage) { + RequestMessage requestMessage = (RequestMessage) message; + out.name("id"); + writeId(out, requestMessage.getRawId()); + out.name("method"); + out.value(requestMessage.getMethod()); + out.name("params"); + Object params = requestMessage.getParams(); + if (params == null) + writeNullValue(out); + else + gson.toJson(params, params.getClass(), out); + } else if (message instanceof ResponseMessage) { + ResponseMessage responseMessage = (ResponseMessage) message; + out.name("id"); + writeId(out, responseMessage.getRawId()); + if (responseMessage.getError() != null) { + out.name("error"); + gson.toJson(responseMessage.getError(), ResponseError.class, out); + } else { + out.name("result"); + Object result = responseMessage.getResult(); + if (result == null) + writeNullValue(out); + else + gson.toJson(result, result.getClass(), out); + } + } else if (message instanceof NotificationMessage) { + NotificationMessage notificationMessage = (NotificationMessage) message; + out.name("method"); + out.value(notificationMessage.getMethod()); + out.name("params"); + Object params = notificationMessage.getParams(); + if (params == null) + writeNullValue(out); + else + gson.toJson(params, params.getClass(), out); + } + + out.endObject(); + } + + protected void writeId(JsonWriter out, Either id) throws IOException { + if (id == null) + writeNullValue(out); + else if (id.isLeft()) + out.value(id.getLeft()); + else if (id.isRight()) + out.value(id.getRight()); + } + + /** + * Use this method to write a {@code null} value even if the JSON writer is set to not serialize {@code null}. + */ + protected void writeNullValue(JsonWriter out) throws IOException { + boolean previousSerializeNulls = out.getSerializeNulls(); + out.setSerializeNulls(true); + out.nullValue(); + out.setSerializeNulls(previousSerializeNulls); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/MessageTypeAdapterFactory.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/MessageTypeAdapterFactory.java new file mode 100644 index 000000000..8afa239e4 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/MessageTypeAdapterFactory.java @@ -0,0 +1,26 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; + +/** + * @deprecated Use {@link MessageTypeAdapter.Factory} instead. + */ +@Deprecated +public class MessageTypeAdapterFactory extends MessageTypeAdapter.Factory { + + public MessageTypeAdapterFactory(MessageJsonHandler handler) { + super(handler); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/ThrowableTypeAdapter.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/ThrowableTypeAdapter.java new file mode 100644 index 000000000..caaaf94eb --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/ThrowableTypeAdapter.java @@ -0,0 +1,136 @@ +/****************************************************************************** + * Copyright (c) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.io.IOException; +import java.lang.reflect.Constructor; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * A type adapter for {@link Throwable}. This is used to report issues to the sender of a request. + */ +public class ThrowableTypeAdapter extends TypeAdapter { + + public static class Factory implements TypeAdapterFactory { + + @SuppressWarnings({ "unchecked" }) + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (!Throwable.class.isAssignableFrom(typeToken.getRawType())) + return null; + + return (TypeAdapter) new ThrowableTypeAdapter((TypeToken) typeToken); + } + + } + + private final TypeToken typeToken; + + public ThrowableTypeAdapter(TypeToken typeToken) { + this.typeToken = typeToken; + } + + @SuppressWarnings("unchecked") + @Override + public Throwable read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + in.beginObject(); + String message = null; + Throwable cause = null; + while (in.hasNext()) { + String name = in.nextName(); + switch (name) { + case "message": { + message = in.nextString(); + break; + } + case "cause": { + cause = read(in); + break; + } + default: + in.skipValue(); + } + } + in.endObject(); + + try { + Constructor constructor; + if (message == null && cause == null) { + constructor = (Constructor) typeToken.getRawType().getDeclaredConstructor(); + return constructor.newInstance(); + } else if (message == null) { + constructor = (Constructor) typeToken.getRawType().getDeclaredConstructor(Throwable.class); + return constructor.newInstance(cause); + } else if (cause == null) { + constructor = (Constructor) typeToken.getRawType().getDeclaredConstructor(String.class); + return constructor.newInstance(message); + } else { + constructor = (Constructor) typeToken.getRawType().getDeclaredConstructor(String.class, Throwable.class); + return constructor.newInstance(message, cause); + } + } catch (NoSuchMethodException e) { + if (message == null && cause == null) + return new RuntimeException(); + else if (message == null) + return new RuntimeException(cause); + else if (cause == null) + return new RuntimeException(message); + else + return new RuntimeException(message, cause); + } catch (Exception e) { + throw new JsonParseException(e); + } + } + + @Override + public void write(JsonWriter out, Throwable throwable) throws IOException { + if (throwable == null) { + out.nullValue(); + } else if (throwable.getMessage() == null && throwable.getCause() != null) { + write(out, throwable.getCause()); + } else { + out.beginObject(); + if (throwable.getMessage() != null) { + out.name("message"); + out.value(throwable.getMessage()); + } + if (shouldWriteCause(throwable)) { + out.name("cause"); + write(out, throwable.getCause()); + } + out.endObject(); + } + } + + private boolean shouldWriteCause(Throwable throwable) { + Throwable cause = throwable.getCause(); + if (cause == null || cause.getMessage() == null || cause == throwable) + return false; + if (throwable.getMessage() != null && throwable.getMessage().contains(cause.getMessage())) + return false; + return true; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/TupleTypeAdapters.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/TupleTypeAdapters.java new file mode 100644 index 000000000..2214f03de --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/TupleTypeAdapters.java @@ -0,0 +1,74 @@ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.io.IOException; +import java.lang.reflect.Type; + +import org.eclipse.lsp4j.jsonrpc.messages.Tuple.Two; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +public final class TupleTypeAdapters { + + private TupleTypeAdapters() {} + + public static class TwoTypeAdapter extends TypeAdapter> { + + protected final TypeAdapter first; + protected final TypeAdapter second; + + @SuppressWarnings("unchecked") + public TwoTypeAdapter(Gson gson, TypeToken> typeToken) { + Type[] elementTypes = TypeUtils.getElementTypes(typeToken, Two.class); + this.first = gson.getAdapter((TypeToken) TypeToken.get(elementTypes[0])); + this.second = gson.getAdapter((TypeToken) TypeToken.get(elementTypes[1])); + } + + @Override + public void write(final JsonWriter out, final Two value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.beginArray(); + first.write(out, value.getFirst()); + second.write(out, value.getSecond()); + out.endArray(); + } + } + + @Override + public Two read(final JsonReader in) throws IOException { + JsonToken next = in.peek(); + if (next == JsonToken.NULL) { + in.nextNull(); + return null; + } + in.beginArray(); + F f = first.read(in); + S s = second.read(in); + Two result = new Two(f, s); + in.endArray(); + return result; + } + + } + + public static class TwoTypeAdapterFactory implements TypeAdapterFactory { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (!TypeUtils.isTwoTuple(typeToken.getType())) { + return null; + } + return new TwoTypeAdapter(gson, typeToken); + } + + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/TypeUtils.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/TypeUtils.java new file mode 100644 index 000000000..11e453088 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/adapters/TypeUtils.java @@ -0,0 +1,242 @@ +/****************************************************************************** + * Copyright (c) 2017 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.json.adapters; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Tuple; + +import com.google.gson.reflect.TypeToken; + +/** + * Utilities for handling types in the JSON parser / serializer. + */ +public final class TypeUtils { + + private TypeUtils() {} + + /** + * Determine the actual type arguments of the given type token with regard to the given target type. + */ + public static Type[] getElementTypes(TypeToken typeToken, Class targetType) { + return getElementTypes(typeToken.getType(), typeToken.getRawType(), targetType); + } + + private static Type[] getElementTypes(Type type, Class rawType, Class targetType) { + if (targetType.equals(rawType) && type instanceof ParameterizedType) { + Type mappedType; + if (type instanceof ParameterizedTypeImpl) + mappedType = type; + else + // Transform wildcards in the actual type arguments + mappedType = getMappedType(type, Collections.emptyMap()); + return ((ParameterizedType) mappedType).getActualTypeArguments(); + } + // Map the parameters of the raw type to the actual type arguments + Map varMapping = createVariableMapping(type, rawType); + if (targetType.isInterface()) { + // Look for superinterfaces that extend the target interface + Class[] interfaces = rawType.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + if (Collection.class.isAssignableFrom(interfaces[i])) { + Type genericInterface = rawType.getGenericInterfaces()[i]; + Type mappedInterface = getMappedType(genericInterface, varMapping); + return getElementTypes(mappedInterface, interfaces[i], targetType); + } + } + } + if (!rawType.isInterface()) { + // Visit the superclass if it extends the target class / implements the target interface + Class rawSupertype = rawType.getSuperclass(); + if (targetType.isAssignableFrom(rawSupertype)) { + Type genericSuperclass = rawType.getGenericSuperclass(); + Type mappedSuperclass = getMappedType(genericSuperclass, varMapping); + return getElementTypes(mappedSuperclass, rawSupertype, targetType); + } + } + // No luck, return an array of Object types + Type[] result = new Type[targetType.getTypeParameters().length]; + Arrays.fill(result, Object.class); + return result; + } + + private static Map createVariableMapping(Type type, Class rawType) { + if (type instanceof ParameterizedType) { + TypeVariable>[] vars = rawType.getTypeParameters(); + Type[] args = ((ParameterizedType) type).getActualTypeArguments(); + Map newVarMapping = new HashMap<>(capacity(vars.length)); + for (int i = 0; i < vars.length; i++) { + Type actualType = Object.class; + if (i < args.length) { + actualType = args[i]; + if (actualType instanceof WildcardType) + actualType = ((WildcardType) actualType).getUpperBounds()[0]; + } + newVarMapping.put(vars[i].getName(), actualType); + } + return newVarMapping; + } + return Collections.emptyMap(); + } + + private static int capacity(int expectedSize) { + if (expectedSize < 3) + return expectedSize + 1; + else + return expectedSize + expectedSize / 3; + } + + private static Type getMappedType(Type type, Map varMapping) { + if (type instanceof TypeVariable) { + String name = ((TypeVariable) type).getName(); + if (varMapping.containsKey(name)) + return varMapping.get(name); + } + if (type instanceof WildcardType) { + return getMappedType(((WildcardType) type).getUpperBounds()[0], varMapping); + } + if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) type; + Type[] origArgs = pt.getActualTypeArguments(); + Type[] mappedArgs = new Type[origArgs.length]; + for (int i = 0; i < origArgs.length; i++) { + mappedArgs[i] = getMappedType(origArgs[i], varMapping); + } + return new ParameterizedTypeImpl(pt, mappedArgs); + } + return type; + } + + private static class ParameterizedTypeImpl implements ParameterizedType { + + private final Type ownerType; + private final Type rawType; + private final Type[] actualTypeArguments; + + ParameterizedTypeImpl(ParameterizedType original, Type[] typeArguments) { + this(original.getOwnerType(), original.getRawType(), typeArguments); + } + + ParameterizedTypeImpl(Type ownerType, Type rawType, Type[] typeArguments) { + this.ownerType = ownerType; + this.rawType = rawType; + this.actualTypeArguments = typeArguments; + } + + @Override + public Type getOwnerType() { + return ownerType; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type[] getActualTypeArguments() { + return actualTypeArguments; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + if (ownerType != null) { + result.append(toString(ownerType)); + result.append('$'); + } + result.append(toString(rawType)); + result.append('<'); + for (int i = 0; i < actualTypeArguments.length; i++) { + if (i > 0) + result.append(", "); + result.append(toString(actualTypeArguments[i])); + } + result.append('>'); + return result.toString(); + } + + private String toString(Type type) { + if (type instanceof Class) + return ((Class) type).getName(); + else + return String.valueOf(type); + } + + } + + /** + * Return all possible types that can be expected when an element of the given type is parsed. + * If the type satisfies {@link #isEither(Type)}, a list of the corresponding type arguments is returned, + * otherwise a list containg the type itself is returned. Type parameters are not resolved + * by this method (use {@link #getElementTypes(TypeToken, Class)} to get resolved parameters). + */ + public static Collection getExpectedTypes(Type type) { + Collection result = new ArrayList<>(); + collectExpectedTypes(type, result); + return result; + } + + private static void collectExpectedTypes(Type type, Collection types) { + if (isEither(type)) { + if (type instanceof ParameterizedType) { + for (Type typeArgument : ((ParameterizedType) type).getActualTypeArguments()) { + collectExpectedTypes(typeArgument, types); + } + } + if (type instanceof Class) { + for (Type typeParameter : ((Class) type).getTypeParameters()) { + collectExpectedTypes(typeParameter, types); + } + } + } else { + types.add(type); + } + } + + /** + * Test whether the given type is Either. + */ + public static boolean isEither(Type type) { + if (type instanceof ParameterizedType) { + return isEither(((ParameterizedType) type).getRawType()); + } + if (type instanceof Class) { + return Either.class.isAssignableFrom((Class) type); + } + return false; + } + + /** + * Test whether the given type is a two-tuple (pair). + */ + public static boolean isTwoTuple(Type type) { + if (type instanceof ParameterizedType) { + return isTwoTuple(((ParameterizedType) type).getRawType()); + } + if (type instanceof Class) { + return Tuple.Two.class.isAssignableFrom((Class) type); + } + return false; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/CancelParams.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/CancelParams.java new file mode 100644 index 000000000..2c6a35ff6 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/CancelParams.java @@ -0,0 +1,86 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * To cancel a request a notification message with the following properties is sent. + */ +public class CancelParams { + + /** + * The request id to cancel. + */ + @NonNull + private Either id; + + @NonNull + public String getId() { + if (id == null) + return null; + if (id.isLeft()) + return id.getLeft(); + if (id.isRight()) + return id.getRight().toString(); + return null; + } + + @NonNull + public Either getRawId() { + return id; + } + + public void setId(@NonNull String id) { + this.id = Either.forLeft(id); + } + + public void setId(@NonNull int id) { + this.id = Either.forRight(id); + } + + public void setRawId(@NonNull Either id) { + this.id = id; + } + + @Override + public String toString() { + return MessageJsonHandler.toString(this); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CancelParams other = (CancelParams) obj; + if (this.id == null) { + if (other.id != null) + return false; + } else if (!this.id.equals(other.id)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.id == null) ? 0 : this.id.hashCode()); + return result; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Either.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Either.java new file mode 100644 index 000000000..0b5f48447 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Either.java @@ -0,0 +1,223 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.function.Function; + +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * An either type maps union types in protocol specifications. + */ +public class Either { + + public static Either forLeft(@NonNull L left) { + return new Either<>(left, null); + } + + public static Either forRight(@NonNull R right) { + return new Either<>(null, right); + } + + private final L left; + private final R right; + + protected Either(L left, R right) { + super(); + this.left = left; + this.right = right; + } + + public L getLeft() { + return left; + } + + public R getRight() { + return right; + } + + public Object get() { + if (left != null) + return left; + if (right != null) + return right; + return null; + } + + public boolean isLeft() { + return left != null; + } + + public boolean isRight() { + return right != null; + } + + public T map( + @NonNull Function mapLeft, + @NonNull Function mapRight) { + if (isLeft()) { + return mapLeft.apply(getLeft()); + } + if (isRight()) { + return mapRight.apply(getRight()); + } + return null; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Either) { + Either other = (Either) obj; + return (this.left == other.left && this.right == other.right) + || (this.left != null && other.left != null && this.left.equals(other.left)) + || (this.right != null && other.right != null && this.right.equals(other.right)); + } + return false; + } + + @Override + public int hashCode() { + if (this.left != null) + return this.left.hashCode(); + if (this.right != null) + return this.right.hashCode(); + return 0; + } + + public String toString() { + StringBuilder builder = new StringBuilder("Either [").append(System.lineSeparator()); + builder.append(" left = ").append(left).append(System.lineSeparator()); + builder.append(" right = ").append(right).append(System.lineSeparator()); + return builder.append("]").toString(); + } + + /** + * Return a left disjoint type if the given type is either. + * + * @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#getElementTypes(Type, Class, Class)} instead + */ + @Deprecated + public static Type getLeftDisjointType(Type type) { + if (isEither(type)) { + if (type instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) type; + return parameterizedType.getActualTypeArguments()[0]; + } + if (type instanceof Class) { + final Class cls = (Class) type; + return cls.getTypeParameters()[0]; + } + } + return null; + } + + /** + * Return a right disjoint type if the given type is either. + * + * @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#getElementTypes(Type, Class, Class)} instead + */ + @Deprecated + public static Type getRightDisjointType(Type type) { + if (isEither(type)) { + if (type instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) type; + return parameterizedType.getActualTypeArguments()[1]; + } + if (type instanceof Class) { + final Class cls = (Class) type; + return cls.getTypeParameters()[1]; + } + } + return null; + } + + /** + * Return all disjoint types. + * + * @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#getExpectedTypes(Type)} instead + */ + @Deprecated + public static Collection getAllDisjoinTypes(Type type) { + return collectDisjoinTypes(type, new ArrayList<>()); + } + + @Deprecated + protected static Collection collectDisjoinTypes(Type type, Collection types) { + if (isEither(type)) { + if (type instanceof ParameterizedType) { + return collectDisjoinTypes((ParameterizedType) type, types); + } + if (type instanceof Class) { + return collectDisjoinTypes((Class) type, types); + } + } + types.add(type); + return types; + } + + @Deprecated + protected static Collection collectDisjoinTypes(ParameterizedType type, Collection types) { + for (Type typeArgument : type.getActualTypeArguments()) { + collectDisjoinTypes(typeArgument, types); + } + return types; + } + + @Deprecated + protected static Collection collectDisjoinTypes(Class type, Collection types) { + for (Type typeParameter : type.getTypeParameters()) { + collectDisjoinTypes(typeParameter, types); + } + return types; + } + + /** + * Test whether the given type is Either. + * + * @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#isEither(Type)} instead + */ + @Deprecated + public static boolean isEither(Type type) { + if (type instanceof ParameterizedType) { + return isEither((ParameterizedType) type); + } + if (type instanceof Class) { + return isEither((Class) type); + } + return false; + } + + /** + * Test whether the given type is Either. + * + * @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#isEither(Type)} instead + */ + @Deprecated + public static boolean isEither(ParameterizedType type) { + return isEither(type.getRawType()); + } + + /** + * Test whether the given class is Either. + * + * @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#isEither(Type)} instead + */ + @Deprecated + public static boolean isEither(Class cls) { + return Either.class.isAssignableFrom(cls); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Either3.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Either3.java new file mode 100644 index 000000000..81f385ec2 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Either3.java @@ -0,0 +1,111 @@ +/****************************************************************************** + * Copyright (c) 2017 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import java.util.function.Function; + +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Union type for three types. + */ +public class Either3 extends Either> { + + public static Either3 forFirst(@NonNull T1 first) { + return new Either3(first, null); + } + + public static Either3 forSecond(@NonNull T2 second) { + return new Either3(null, new Either(second, null)); + } + + public static Either3 forThird(@NonNull T3 third) { + return new Either3(null, new Either(null, third)); + } + + public static Either3 forLeft3(@NonNull T1 first) { + return new Either3(first, null); + } + + public static Either3 forRight3(@NonNull Either right) { + return new Either3(null, right); + } + + protected Either3(T1 left, Either right) { + super(left, right); + } + + public T1 getFirst() { + return getLeft(); + } + + public T2 getSecond() { + Either right = getRight(); + if (right == null) + return null; + else + return right.getLeft(); + } + + public T3 getThird() { + Either right = getRight(); + if (right == null) + return null; + else + return right.getRight(); + } + + @Override + public Object get() { + if (isRight()) + return getRight().get(); + return super.get(); + } + + public boolean isFirst() { + return isLeft(); + } + + public boolean isSecond() { + return isRight() && getRight().isLeft(); + } + + public boolean isThird() { + return isRight() && getRight().isRight(); + } + + public T map( + @NonNull Function mapFirst, + @NonNull Function mapSecond, + @NonNull Function mapThird) { + if (isFirst()) { + return mapFirst.apply(getFirst()); + } + if (isSecond()) { + return mapSecond.apply(getSecond()); + } + if (isThird()) { + return mapThird.apply(getThird()); + } + return null; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("Either3 [").append(System.lineSeparator()); + builder.append(" first = ").append(getFirst()).append(System.lineSeparator()); + builder.append(" second = ").append(getSecond()).append(System.lineSeparator()); + builder.append(" third = ").append(getThird()).append(System.lineSeparator()); + return builder.append("]").toString(); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/IdentifiableMessage.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/IdentifiableMessage.java new file mode 100644 index 000000000..61faf65a4 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/IdentifiableMessage.java @@ -0,0 +1,81 @@ +/****************************************************************************** + * Copyright (c) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * A message with an {@code id} property. + */ +public abstract class IdentifiableMessage extends Message { + + /** + * The message identifier. + */ + @NonNull + private Either id; + + public String getId() { + if (id == null) + return null; + if (id.isLeft()) + return id.getLeft(); + if (id.isRight()) + return id.getRight().toString(); + return null; + } + + @NonNull + public Either getRawId() { + return id; + } + + public void setId(String id) { + this.id = Either.forLeft(id); + } + + public void setId(int id) { + this.id = Either.forRight(id); + } + + public void setRawId(@NonNull Either id) { + this.id = id; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + if (!super.equals(obj)) + return false; + IdentifiableMessage other = (IdentifiableMessage) obj; + if (this.id == null) { + if (other.id != null) + return false; + } else if (!this.id.equals(other.id)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((this.id == null) ? 0 : this.id.hashCode()); + return result; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Message.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Message.java new file mode 100644 index 000000000..5d4be746c --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Message.java @@ -0,0 +1,66 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import org.eclipse.lsp4j.jsonrpc.json.MessageConstants; +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * A general message as defined by JSON-RPC. The language server protocol always + * uses "2.0" as the jsonrpc version. + */ +public abstract class Message { + + @NonNull + private String jsonrpc = MessageConstants.JSONRPC_VERSION; + + @NonNull + public String getJsonrpc() { + return this.jsonrpc; + } + + public void setJsonrpc(@NonNull String jsonrpc) { + this.jsonrpc = jsonrpc; + } + + @Override + public String toString() { + return MessageJsonHandler.toString(this); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Message other = (Message) obj; + if (this.jsonrpc == null) { + if (other.jsonrpc != null) + return false; + } else if (!this.jsonrpc.equals(other.jsonrpc)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.jsonrpc == null) ? 0 : this.jsonrpc.hashCode()); + return result; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/MessageIssue.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/MessageIssue.java new file mode 100644 index 000000000..122025532 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/MessageIssue.java @@ -0,0 +1,60 @@ +/****************************************************************************** + * Copyright (c) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Describes an issue found while parsing or validating a message. + */ +public class MessageIssue { + + @NonNull + private String text; + + private int code; + + private Exception cause; + + public MessageIssue(@NonNull String text) { + this(text, 0, null); + } + + public MessageIssue(@NonNull String text, int code) { + this(text, code, null); + } + + public MessageIssue(@NonNull String text, int code, Exception cause) { + this.text = text; + this.code = code; + this.cause = cause; + } + + @NonNull + public String getText() { + return text; + } + + public int getIssueCode() { + return code; + } + + public Exception getCause() { + return cause; + } + + @Override + public String toString() { + return getText(); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/NotificationMessage.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/NotificationMessage.java new file mode 100644 index 000000000..b10f9d465 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/NotificationMessage.java @@ -0,0 +1,83 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * A notification message. A processed notification message must not send a + * response back. They work like events. + */ +public class NotificationMessage extends Message { + + /** + * The method to be invoked. + */ + @NonNull + private String method; + + @NonNull + public String getMethod() { + return this.method; + } + + public void setMethod(@NonNull String method) { + this.method = method; + } + + /** + * The method's params. + */ + private Object params; + + public Object getParams() { + return this.params; + } + + public void setParams(Object params) { + this.params = params; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + if (!super.equals(obj)) + return false; + NotificationMessage other = (NotificationMessage) obj; + if (this.method == null) { + if (other.method != null) + return false; + } else if (!this.method.equals(other.method)) + return false; + if (this.params == null) { + if (other.params != null) + return false; + } else if (!this.params.equals(other.params)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((this.method == null) ? 0 : this.method.hashCode()); + result = prime * result + ((this.params == null) ? 0 : this.params.hashCode()); + return result; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/RequestMessage.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/RequestMessage.java new file mode 100644 index 000000000..42d76c26e --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/RequestMessage.java @@ -0,0 +1,84 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * A request message to describe a request between the client and the server. + * Every processed request must send a response back to the sender of the + * request. + */ +public class RequestMessage extends IdentifiableMessage { + + /** + * The method to be invoked. + */ + @NonNull + private String method; + + @NonNull + public String getMethod() { + return this.method; + } + + public void setMethod(@NonNull String method) { + this.method = method; + } + + /** + * The method's parameters. The object type depends on the chosen method. + */ + private Object params; + + public Object getParams() { + return this.params; + } + + public void setParams(Object params) { + this.params = params; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + if (!super.equals(obj)) + return false; + RequestMessage other = (RequestMessage) obj; + if (this.method == null) { + if (other.method != null) + return false; + } else if (!this.method.equals(other.method)) + return false; + if (this.params == null) { + if (other.params != null) + return false; + } else if (!this.params.equals(other.params)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((this.method == null) ? 0 : this.method.hashCode()); + result = prime * result + ((this.params == null) ? 0 : this.params.hashCode()); + return result; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseError.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseError.java new file mode 100644 index 000000000..64b8888d5 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseError.java @@ -0,0 +1,123 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.json.adapters.JsonElementTypeAdapter; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +import com.google.gson.annotations.JsonAdapter; + +public class ResponseError { + + /** + * A number indicating the error type that occurred. + */ + @NonNull + private int code; + + @NonNull + public int getCode() { + return this.code; + } + + public void setCode(@NonNull int code) { + this.code = code; + } + + public void setCode(ResponseErrorCode code) { + this.setCode(code.getValue()); + } + + /** + * A string providing a short description of the error. + */ + @NonNull + private String message; + + @NonNull + public String getMessage() { + return this.message; + } + + public void setMessage(@NonNull String message) { + this.message = message; + } + + /** + * A Primitive or Structured value that contains additional information + * about the error. Can be omitted. + */ + @JsonAdapter(JsonElementTypeAdapter.Factory.class) + private Object data; + + public Object getData() { + return this.data; + } + + public void setData(Object data) { + this.data = data; + } + + public ResponseError() { + } + + public ResponseError(ResponseErrorCode code, String message, Object data) { + this(code.getValue(), message, data); + } + + public ResponseError(int code, String message, Object data) { + this.code = code; + this.message = message; + this.data = data; + } + + @Override + public String toString() { + return MessageJsonHandler.toString(this); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ResponseError other = (ResponseError) obj; + if (other.code != this.code) + return false; + if (this.message == null) { + if (other.message != null) + return false; + } else if (!this.message.equals(other.message)) + return false; + if (this.data == null) { + if (other.data != null) + return false; + } else if (!this.data.equals(other.data)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.code; + result = prime * result + ((this.message == null) ? 0 : this.message.hashCode()); + result = prime * result + ((this.data == null) ? 0 : this.data.hashCode()); + return result; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseErrorCode.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseErrorCode.java new file mode 100644 index 000000000..14761c8cf --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseErrorCode.java @@ -0,0 +1,129 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +/** + * A number indicating the error type that occurred. + */ +public enum ResponseErrorCode { + + ParseError(-32700), + + InvalidRequest(-32600), + + MethodNotFound(-32601), + + InvalidParams(-32602), + + InternalError(-32603), + + /** + * This is the start range of JSON RPC reserved error codes. + * It doesn't denote a real error code. No LSP error codes should + * be defined between the start and end range. For backwards + * compatibility the {@link #serverNotInitialized} and the + * {@link #UnknownErrorCode} are left in the range. + *

+ * Since 3.16.0 + */ + jsonrpcReservedErrorRangeStart(-32099), + + /** @deprecated use {@link #jsonrpcReservedErrorRangeStart} */ + @Deprecated + serverErrorStart(-32099), + + /** + * Error code indicating that a server received a notification or + * request before the server has received the {@code initialize} request. + *

+ * Should be {@code ServerNotInitialized} + */ + serverNotInitialized(-32002), + + UnknownErrorCode(-32001), + + /** + * This is the end range of JSON RPC reserved error codes. + * It doesn't denote a real error code. + *

+ * Since 3.16.0 + */ + jsonrpcReservedErrorRangeEnd(-32000), + + /** @deprecated use {@link #jsonrpcReservedErrorRangeEnd} */ + @Deprecated + serverErrorEnd(-32000), + + /** + * This is the start range of LSP reserved error codes. + * It doesn't denote a real error code. + *

+ * Since 3.16.0 + */ + lspReservedErrorRangeStart(-32899), + + /** + * A request failed but it was syntactically correct, e.g the + * method name was known and the parameters were valid. The error + * message should contain human readable information about why + * the request failed. + *

+ * Since 3.17.0 + */ + RequestFailed(-32803), + + /** + * The server cancelled the request. This error code should + * only be used for requests that explicitly support being + * server cancellable. + *

+ * Since 3.17.0 + */ + ServerCancelled(-32802), + + /** + * The server detected that the content of a document got + * modified outside normal conditions. A server should + * NOT send this error code if it detects a content change + * in it unprocessed messages. The result even computed + * on an older state might still be useful for the client. + *

+ * If a client decides that a result is not of any use anymore + * the client should cancel the request. + */ + ContentModified(-32801), + + /** + * The client has canceled a request and a server as detected + * the cancel. + */ + RequestCancelled(-32800), + + /** + * This is the end range of LSP reserved error codes. + * It doesn't denote a real error code. + *

+ * Since 3.16.0 + */ + lspReservedErrorRangeEnd(-32800); + + private final int value; + + ResponseErrorCode(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseMessage.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseMessage.java new file mode 100644 index 000000000..baf33e3b9 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/ResponseMessage.java @@ -0,0 +1,83 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.messages; + +/** + * Response message sent as a result of a request. If a request doesn't provide + * a result value the receiver of a request still needs to return a response + * message to conform to the JSON RPC specification. The result property of the + * ResponseMessage should be set to null in this case to signal a successful + * request. A response message is linked to a request via their {@code id} properties. + */ +public class ResponseMessage extends IdentifiableMessage { + + /** + * The result of a request. This can be omitted in the case of an error. + * The object type depends on the method of the corresponding request. + */ + private Object result; + + public Object getResult() { + return this.result; + } + + public void setResult(Object result) { + this.result = result; + } + + /** + * The error object in case a request fails. + */ + private ResponseError error; + + public ResponseError getError() { + return this.error; + } + + public void setError(ResponseError error) { + this.error = error; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + if (!super.equals(obj)) + return false; + ResponseMessage other = (ResponseMessage) obj; + if (this.result == null) { + if (other.result != null) + return false; + } else if (!this.result.equals(other.result)) + return false; + if (this.error == null) { + if (other.error != null) + return false; + } else if (!this.error.equals(other.error)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((this.result == null) ? 0 : this.result.hashCode()); + result = prime * result + ((this.error == null) ? 0 : this.error.hashCode()); + return result; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Tuple.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Tuple.java new file mode 100644 index 000000000..03618ceb0 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/messages/Tuple.java @@ -0,0 +1,81 @@ +package org.eclipse.lsp4j.jsonrpc.messages; + +import java.io.IOException; + +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Representation of tuple types. + */ +@SuppressWarnings("all") +public interface Tuple { + + public static Two two(F first, S second) { + return new Two(first, second); + } + + /** + * A two-tuple, i.e. a pair. + */ + public static class Two implements Tuple { + + private final F first; + private final S second; + + public Two(F first, S second) { + this.first = first; + this.second = second; + } + + public F getFirst() { + return this.first; + } + + public S getSecond() { + return this.second; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Tuple.Two other = (Tuple.Two) obj; + if (this.first == null) { + if (other.first != null) + return false; + } else if (!this.first.equals(other.first)) + return false; + if (this.second == null) { + if (other.second != null) + return false; + } else if (!this.second.equals(other.second)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.first == null) ? 0 : this.first.hashCode()); + return prime * result + ((this.second == null) ? 0 : this.second.hashCode()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("Tuples.Two [").append(System.lineSeparator()); + builder.append(" first = ").append(first).append(System.lineSeparator()); + builder.append(" second = ").append(second).append(System.lineSeparator()); + return builder.append("]").toString(); + } + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/AnnotationUtil.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/AnnotationUtil.java new file mode 100644 index 000000000..48f6fc2f3 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/AnnotationUtil.java @@ -0,0 +1,131 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.services; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Set; +import java.util.function.Consumer; + +public final class AnnotationUtil { + private AnnotationUtil() {} + + public static void findDelegateSegments(Class clazz, Set> visited, Consumer acceptor) { + if (clazz == null || !visited.add(clazz)) + return; + findDelegateSegments(clazz.getSuperclass(), visited, acceptor); + for (Class interf : clazz.getInterfaces()) { + findDelegateSegments(interf, visited, acceptor); + } + for (Method method : clazz.getDeclaredMethods()) { + if (isDelegateMethod(method)) { + acceptor.accept(method); + } + } + } + + public static boolean isDelegateMethod(Method method) { + if (!method.isSynthetic()) { + JsonDelegate jsonDelegate = method.getAnnotation(JsonDelegate.class); + if (jsonDelegate != null) { + if (!(method.getParameterCount() == 0 && method.getReturnType().isInterface())) { + throw new IllegalStateException( + "The method " + method.toString() + " is not a proper @JsonDelegate method."); + } + return true; + } + } + return false; + } + + + /** + * Depth first search for annotated methods in hierarchy. + */ + public static void findRpcMethods(Class clazz, Set> visited, Consumer acceptor) { + if (clazz == null || !visited.add(clazz)) + return; + findRpcMethods(clazz.getSuperclass(), visited, acceptor); + for (Class interf : clazz.getInterfaces()) { + findRpcMethods(interf, visited, acceptor); + } + String segment = getSegment(clazz); + for (Method method : clazz.getDeclaredMethods()) { + MethodInfo methodInfo = createMethodInfo(method, segment); + if (methodInfo != null) { + acceptor.accept(methodInfo); + } + } + } + + protected static String getSegment(Class clazz) { + JsonSegment jsonSegment = clazz.getAnnotation(JsonSegment.class); + return jsonSegment == null ? "" : jsonSegment.value() + "/"; + } + + protected static MethodInfo createMethodInfo(Method method, String segment) { + if (!method.isSynthetic()) { + JsonRequest jsonRequest = method.getAnnotation(JsonRequest.class); + if (jsonRequest != null) { + return createRequestInfo(method, segment, jsonRequest); + } + JsonNotification jsonNotification = method.getAnnotation(JsonNotification.class); + if (jsonNotification != null) { + return createNotificationInfo(method, segment, jsonNotification); + } + } + return null; + } + + protected static MethodInfo createNotificationInfo(Method method, String segment, JsonNotification jsonNotification) { + MethodInfo methodInfo = createMethodInfo(method, jsonNotification.useSegment(), segment, jsonNotification.value()); + methodInfo.isNotification = true; + return methodInfo; + } + + protected static MethodInfo createRequestInfo(Method method, String segment, JsonRequest jsonRequest) { + return createMethodInfo(method, jsonRequest.useSegment(), segment, jsonRequest.value()); + } + + protected static MethodInfo createMethodInfo(Method method, boolean useSegment, String segment, String value) { + method.setAccessible(true); + + MethodInfo methodInfo = new MethodInfo(); + methodInfo.method = method; + methodInfo.parameterTypes = getParameterTypes(method); + methodInfo.name = getMethodName(method, useSegment, segment, value); + return methodInfo; + } + + protected static String getMethodName(Method method, boolean useSegment, String segment, String value) { + String name = value != null && value.length() > 0 ? value : method.getName(); + return useSegment ? segment + name : name; + } + + protected static Type[] getParameterTypes(Method method) { + return Arrays.stream(method.getParameters()).map(t -> t.getParameterizedType()).toArray(Type[]::new); + } + + static class MethodInfo { + private static Type[] EMPTY_TYPE_ARRAY = {}; + public String name; + public Method method; + public Type[] parameterTypes = EMPTY_TYPE_ARRAY; + public boolean isNotification = false; + } + + static class DelegateInfo { + public Method method; + public Object delegate; + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/EndpointProxy.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/EndpointProxy.java new file mode 100644 index 000000000..bb1609c78 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/EndpointProxy.java @@ -0,0 +1,130 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.services; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; + +import org.eclipse.lsp4j.jsonrpc.Endpoint; +import org.eclipse.lsp4j.jsonrpc.services.AnnotationUtil.DelegateInfo; +import org.eclipse.lsp4j.jsonrpc.services.AnnotationUtil.MethodInfo; + +/** + * A Proxy that wraps an {@link Endpoint} in one or more service interfaces, i.e. interfaces + * containing {@link JsonNotification} and {@link JsonRequest} methods. + */ +public class EndpointProxy implements InvocationHandler { + + private final Method object_equals; + private final Method object_hashCode; + private final Method object_toString; + + private final Endpoint delegate; + private final LinkedHashMap methodInfos; + private final LinkedHashMap delegatedSegments; + + public EndpointProxy(Endpoint delegate, Class interface_) { + this(delegate, Collections.singletonList(interface_)); + } + + public EndpointProxy(Endpoint delegate, Collection> interfaces) { + if (delegate == null) + throw new NullPointerException("delegate"); + if (interfaces == null) + throw new NullPointerException("interfaces"); + if (interfaces.isEmpty()) + throw new IllegalArgumentException("interfaces must not be empty."); + + this.delegate = delegate; + try { + object_equals = Object.class.getDeclaredMethod("equals", Object.class); + object_hashCode = Object.class.getDeclaredMethod("hashCode"); + object_toString = Object.class.getDeclaredMethod("toString"); + } catch (NoSuchMethodException | SecurityException exception) { + throw new RuntimeException(exception); + } + methodInfos = new LinkedHashMap<>(); + delegatedSegments = new LinkedHashMap<>(); + for (Class interf : interfaces) { + AnnotationUtil.findRpcMethods(interf, new HashSet>(), (methodInfo) -> { + if (methodInfos.put(methodInfo.method.getName(), methodInfo) != null) { + throw new IllegalStateException("Duplicate RPC method " + methodInfo.method); + } + }); + AnnotationUtil.findDelegateSegments(interf, new HashSet>(), (method) -> { + Object delegateProxy = ServiceEndpoints.toServiceObject(delegate, method.getReturnType()); + DelegateInfo info = new DelegateInfo(); + info.delegate = delegateProxy; + info.method = method; + if (delegatedSegments.put(method.getName(), info) != null) { + throw new IllegalStateException("Duplicate RPC method " + method); + } + }); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + args = args == null ? new Object[0] : args; + MethodInfo methodInfo = this.methodInfos.get(method.getName()); + if (methodInfo != null) { + Object params = getParams(args, methodInfo); + if (methodInfo.isNotification) { + delegate.notify(methodInfo.name, params); + return null; + } + return delegate.request(methodInfo.name, params); + } + DelegateInfo delegateInfo = this.delegatedSegments.get(method.getName()); + if (delegateInfo != null) { + return delegateInfo.delegate; + } + if (object_equals.equals(method) && args.length == 1) { + if(args[0] != null ) { + try { + return this.equals(Proxy.getInvocationHandler(args[0])); + } catch (IllegalArgumentException exception) { + } + } + return this.equals(args[0]); + } + if (object_hashCode.equals(method)) { + return this.hashCode(); + } + if (object_toString.equals(method)) { + return this.toString(); + } + return method.invoke(delegate, args); + } + + protected Object getParams(Object[] args, MethodInfo methodInfo) { + if (args.length == 0) { + return null; + } + if (args.length == 1) { + return args[0]; + } + return Arrays.asList(args); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " for " + delegate.toString(); + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/GenericEndpoint.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/GenericEndpoint.java new file mode 100644 index 000000000..bab1368df --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/GenericEndpoint.java @@ -0,0 +1,190 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.services; + +import com.itsaky.androidide.utils.ILogger; + +import org.eclipse.lsp4j.jsonrpc.Endpoint; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * An endpoint that reflectively delegates to {@link JsonNotification} and {@link JsonRequest} + * methods of one or more given delegate objects. + */ +public class GenericEndpoint implements Endpoint { + + private static final ILogger LOG = ILogger.newInstance("GenericEndpoint"); + private static final Object[] NO_ARGUMENTS = {}; + + private final LinkedHashMap>> methodHandlers = + new LinkedHashMap<>(); + private final List delegates; + + public GenericEndpoint(Object delegate) { + this.delegates = Collections.singletonList(delegate); + recursiveFindRpcMethods(delegate, new HashSet<>(), new HashSet<>()); + } + + public GenericEndpoint(Collection delegates) { + this.delegates = new ArrayList<>(delegates); + for (Object delegate : this.delegates) { + recursiveFindRpcMethods(delegate, new HashSet<>(), new HashSet<>()); + } + } + + @Override + public CompletableFuture request(String method, Object parameter) { + // Check the registered method handlers + Function> handler = methodHandlers.get(method); + if (handler != null) { + return handler.apply(parameter); + } + + // Ask the delegate objects whether they can handle the request generically + List> futures = new ArrayList<>(delegates.size()); + for (Object delegate : delegates) { + if (delegate instanceof Endpoint) { + futures.add(((Endpoint) delegate).request(method, parameter)); + } + } + if (!futures.isEmpty()) { + return CompletableFuture.anyOf(futures.toArray(new CompletableFuture[futures.size()])); + } + + // Create a log message about the unsupported method + String message = "Unsupported request method: " + method; + if (isOptionalMethod(method)) { + LOG.info(message); + return CompletableFuture.completedFuture(null); + } + LOG.warn(message); + CompletableFuture exceptionalResult = new CompletableFuture(); + ResponseError error = new ResponseError(ResponseErrorCode.MethodNotFound, message, null); + exceptionalResult.completeExceptionally(new ResponseErrorException(error)); + return exceptionalResult; + } + + @Override + public void notify(String method, Object parameter) { + // Check the registered method handlers + Function> handler = methodHandlers.get(method); + if (handler != null) { + handler.apply(parameter); + return; + } + + // Ask the delegate objects whether they can handle the notification generically + int notifiedDelegates = 0; + for (Object delegate : delegates) { + if (delegate instanceof Endpoint) { + ((Endpoint) delegate).notify(method, parameter); + notifiedDelegates++; + } + } + + if (notifiedDelegates == 0) { + // Create a log message about the unsupported method + String message = "Unsupported notification method: " + method; + if (isOptionalMethod(method)) { + LOG.info(message); + } else { + LOG.warn(message); + } + } + } + + protected boolean isOptionalMethod(String method) { + return method != null && method.startsWith("$/"); + } + + protected void recursiveFindRpcMethods( + Object current, Set> visited, Set> visitedForDelegate) { + AnnotationUtil.findRpcMethods( + current.getClass(), + visited, + (methodInfo) -> { + @SuppressWarnings("unchecked") + Function> handler = + (arg) -> { + try { + Method method = methodInfo.method; + Object[] arguments = this.getArguments(method, arg); + return (CompletableFuture) method.invoke(current, arguments); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }; + if (methodHandlers.put(methodInfo.name, handler) != null) { + throw new IllegalStateException("Multiple methods for name " + methodInfo.name); + } + }); + AnnotationUtil.findDelegateSegments( + current.getClass(), + visitedForDelegate, + (method) -> { + try { + Object delegate = method.invoke(current); + if (delegate != null) { + recursiveFindRpcMethods(delegate, visited, visitedForDelegate); + } else { + LOG.error( + "A delegate object is null, jsonrpc methods of '" + method + "' are ignored"); + } + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + } + + protected Object[] getArguments(Method method, Object arg) { + int parameterCount = method.getParameterCount(); + if (parameterCount == 0) { + if (arg != null) { + LOG.warn("Unexpected params '" + arg + "' for '" + method + "' is ignored"); + } + return NO_ARGUMENTS; + } + if (arg instanceof List) { + List arguments = (List) arg; + int argumentCount = arguments.size(); + if (argumentCount == parameterCount) { + return arguments.toArray(); + } + if (argumentCount > parameterCount) { + Stream unexpectedArguments = arguments.stream().skip(parameterCount); + String unexpectedParams = + unexpectedArguments.map(a -> "'" + a + "'").reduce((a, a2) -> a + ", " + a2).get(); + LOG.warn("Unexpected params " + unexpectedParams + " for '" + method + "' is ignored"); + return arguments.subList(0, parameterCount).toArray(); + } + return arguments.toArray(new Object[parameterCount]); + } + Object[] arguments = new Object[parameterCount]; + arguments[0] = arg; + return arguments; + } +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonDelegate.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonDelegate.java new file mode 100644 index 000000000..5a6b2249e --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonDelegate.java @@ -0,0 +1,28 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.services; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A method annotated with {@link JsonDelegate} is treated as a delegate method. + * As a result jsonrpc methods of the delegate will be considered, too. + * If an annotated method returns `null` + * then jsonrpc methods of the delegate are not considered. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface JsonDelegate { +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonNotification.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonNotification.java new file mode 100644 index 000000000..75ad20bd2 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonNotification.java @@ -0,0 +1,52 @@ +/****************************************************************************** + * Copyright (c) 2016, 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.services; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a notification method on an interface or class. + *

+ * A notification method must be of type void and have zero or one + * argument. + *

+ * According to jsonrpc an argument must be an 'object' (a java bean, not e,g. + * String). + *

+ * The name of the jsonrpc notification will be the optional segment, followed + * by the name of the Java method that is annotated with JsonNotification. The + * name of the jsonrpc notification can be customized by using the + * {@link #value()} field of this annotation. To specify the whole name, + * including the segment, in the value, set {@link #useSegment()} to false. + * + * @see JsonSegment + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface JsonNotification { + /** + * The name of the the jsonrpc request method. If empty, uses the name of the + * annotated method. + */ + String value() default ""; + + /** + * When using segments, useSegment will be true to prepend the segment name to + * the name of the request. + * + * @see JsonSegment + */ + boolean useSegment() default true; +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonRequest.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonRequest.java new file mode 100644 index 000000000..4e9b801b3 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonRequest.java @@ -0,0 +1,53 @@ +/****************************************************************************** + * Copyright (c) 2016, 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.services; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.CompletableFuture; + +/** + * Annotation to mark a request method on an interface or class. + *

+ * A request method must have the return type {@link CompletableFuture} with an + * object parameter type or Void and have zero or one argument. + *

+ * According to jsonrpc an argument must be an 'object' (a java bean, not e,g. + * String). + *

+ * The name of the jsonrpc request will be the optional segment, followed by the + * name of the Java method that is annotated with JsonRequest. The name of the + * jsonrpc request can be customized by using the {@link #value()} field of this + * annotation. To specify the whole name, including the segment, in the value, + * set {@link #useSegment()} to false. + * + * @see JsonSegment + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface JsonRequest { + /** + * The name of the the jsonrpc request method. If empty, uses the name of the + * annotated method. + */ + String value() default ""; + + /** + * When using segments, useSegment will be true to prepend the segment name to + * the name of the request. + * + * @see JsonSegment + */ + boolean useSegment() default true; +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonSegment.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonSegment.java new file mode 100644 index 000000000..2b731979c --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/JsonSegment.java @@ -0,0 +1,26 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.services; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use on a class or interface to prefix all declared {@link JsonRequest} and {@link JsonNotification} methods. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface JsonSegment { + String value() default ""; +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/ServiceEndpoints.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/ServiceEndpoints.java new file mode 100644 index 000000000..c8ad3f011 --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/ServiceEndpoints.java @@ -0,0 +1,128 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.services; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.lsp4j.jsonrpc.Endpoint; +import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod; +import org.eclipse.lsp4j.jsonrpc.json.ResponseJsonAdapter; + +import com.google.gson.TypeAdapterFactory; + +public final class ServiceEndpoints { + private ServiceEndpoints() {} + + /** + * Wraps a given {@link Endpoint} in the given service interface. + * + * @return the wrapped service object + */ + @SuppressWarnings("unchecked") + public static T toServiceObject(Endpoint endpoint, Class interface_) { + Class[] interfArray = new Class[]{interface_, Endpoint.class}; + EndpointProxy invocationHandler = new EndpointProxy(endpoint, interface_); + return (T) Proxy.newProxyInstance(interface_.getClassLoader(), interfArray, invocationHandler); + } + + /** + * Wraps a given {@link Endpoint} in the given service interfaces. + * + * @return the wrapped service object + */ + public static Object toServiceObject(Endpoint endpoint, Collection> interfaces, ClassLoader classLoader) { + Class[] interfArray = new Class[interfaces.size() + 1]; + interfaces.toArray(interfArray); + interfArray[interfArray.length - 1] = Endpoint.class; + EndpointProxy invocationHandler = new EndpointProxy(endpoint, interfaces); + return Proxy.newProxyInstance(classLoader, interfArray, invocationHandler); + } + + /** + * Wraps a given object with service annotations behind an {@link Endpoint} interface. + * + * @return the wrapped service endpoint + */ + public static Endpoint toEndpoint(Object serviceObject) { + return new GenericEndpoint(serviceObject); + } + + /** + * Wraps a collection of objects with service annotations behind an {@link Endpoint} interface. + * + * @return the wrapped service endpoint + */ + public static Endpoint toEndpoint(Collection serviceObjects) { + return new GenericEndpoint(serviceObjects); + } + + /** + * Finds all Json RPC methods on a given class. + * + * @return the supported JsonRpcMethods + */ + public static Map getSupportedMethods(Class type) { + Set> visitedTypes = new HashSet<>(); + return getSupportedMethods(type, visitedTypes); + } + + /** + * Finds all Json RPC methods on a given type + */ + private static Map getSupportedMethods(Class type, Set> visitedTypes) { + Map result = new LinkedHashMap(); + AnnotationUtil.findRpcMethods(type, visitedTypes, (methodInfo) -> { + JsonRpcMethod meth; + if (methodInfo.isNotification) { + meth = JsonRpcMethod.notification(methodInfo.name, methodInfo.parameterTypes); + } else { + Type genericReturnType = methodInfo.method.getGenericReturnType(); + if (genericReturnType instanceof ParameterizedType) { + Type returnType = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0]; + TypeAdapterFactory responseTypeAdapter = null; + ResponseJsonAdapter responseTypeAdapterAnnotation = methodInfo.method.getAnnotation(ResponseJsonAdapter.class); + if (responseTypeAdapterAnnotation != null) { + try { + responseTypeAdapter = responseTypeAdapterAnnotation.value().newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + meth = JsonRpcMethod.request(methodInfo.name, returnType, responseTypeAdapter, methodInfo.parameterTypes); + } else { + throw new IllegalStateException("Expecting return type of CompletableFuture but was : " + genericReturnType); + } + } + if (result.put(methodInfo.name, meth) != null) { + throw new IllegalStateException("Duplicate RPC method "+methodInfo.name+"."); + }; + }); + + AnnotationUtil.findDelegateSegments(type, new HashSet<>(), (method)-> { + Map supportedDelegateMethods = getSupportedMethods(method.getReturnType(), visitedTypes); + for (JsonRpcMethod meth : supportedDelegateMethods.values()) { + if (result.put(meth.getMethodName(), meth) != null) { + throw new IllegalStateException("Duplicate RPC method "+meth.getMethodName()+"."); + }; + } + }); + return result; + } + +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/validation/NonNull.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/validation/NonNull.java new file mode 100644 index 000000000..9771e754d --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/validation/NonNull.java @@ -0,0 +1,31 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated field or parameter must not be set to {@code null}. If validation + * is active, an exception is thrown when a message is received where a {@code NonNull} field + * has a {@code null} or {@code undefined} value. + * + * In order to achieve consistent behavior, for every field with this annotation the corresponding + * getter method as well as the parameter of the corresponding setter method are also expected to + * be annotated. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_PARAMETER}) +public @interface NonNull { +} diff --git a/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/validation/ReflectiveMessageValidator.java b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/validation/ReflectiveMessageValidator.java new file mode 100644 index 000000000..32c4060ff --- /dev/null +++ b/build-tools/jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/validation/ReflectiveMessageValidator.java @@ -0,0 +1,195 @@ +/****************************************************************************** + * Copyright (c) 2016 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.validation; + +import com.google.gson.JsonElement; +import com.itsaky.androidide.utils.ILogger; + +import org.eclipse.lsp4j.jsonrpc.JsonRpcException; +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.eclipse.lsp4j.jsonrpc.MessageIssueException; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.MessageIssue; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +/** + * Validates messages and forwards them to other message consumers. In case an issue is found, a + * {@link MessageIssueException} is thrown. + */ +public class ReflectiveMessageValidator implements MessageConsumer { + + private static final ILogger LOG = ILogger.newInstance("ReflectiveMessageValidator"); + + private final MessageConsumer delegate; + + /** When created with this constructor, the validator acts as a message sink. */ + public ReflectiveMessageValidator() { + this.delegate = null; + } + + /** Forward messages to the given consumer unless an issue is found. */ + public ReflectiveMessageValidator(MessageConsumer delegate) { + this.delegate = delegate; + } + + @Override + public void consume(Message message) throws MessageIssueException, JsonRpcException { + List issues = validate(message); + if (!issues.isEmpty()) { + // Sort the messages in order to get a stable order (otherwise it depends on the JVM's + // reflection implementation) + Collections.sort(issues, (issue1, issue2) -> issue1.getText().compareTo(issue2.getText())); + throw new MessageIssueException(message, issues); + } else if (delegate != null) { + delegate.consume(message); + } + } + + /** Check whether the given object is valid. If it is not valid, its issues are not reported. */ + public boolean isValid(Object object) { + List issues = validate(object); + return issues.isEmpty(); + } + + protected List validate(Object object) { + List result = new ArrayList<>(); + try { + validate(object, result, new LinkedList<>(), new LinkedList<>()); + } catch (Exception e) { + LOG.error("Error during message validation: " + e.getMessage(), e); + result.add( + new MessageIssue( + "Message validation failed, please check the logs of the remote endpoint.", + ResponseErrorCode.InvalidParams.getValue())); + } + return result; + } + + /** Validate all fields of the given object. */ + protected void validate( + Object object, + List issues, + Deque objectStack, + Deque accessorStack) + throws Exception { + if (object == null + || object instanceof Enum + || object instanceof String + || object instanceof Number + || object instanceof Boolean + || object instanceof JsonElement + || object instanceof Throwable) { + return; + } + if (objectStack.contains(object)) { + issues.add( + new MessageIssue( + "An element of the message has a direct or indirect reference to itself." + + " Path: " + + createPathString(accessorStack), + ResponseErrorCode.InvalidParams.getValue())); + return; + } + objectStack.push(object); + if (object instanceof List) { + ListIterator iter = ((List) object).listIterator(); + while (iter.hasNext()) { + accessorStack.push(iter.nextIndex()); + Object element = iter.next(); + if (element == null) { + issues.add( + new MessageIssue( + "Lists must not contain null references." + + " Path: " + + createPathString(accessorStack), + ResponseErrorCode.InvalidParams.getValue())); + } + validate(element, issues, objectStack, accessorStack); + accessorStack.pop(); + } + } else if (object instanceof Either) { + Either either = (Either) object; + if (either.isLeft()) { + validate(either.getLeft(), issues, objectStack, accessorStack); + } else if (either.isRight()) { + validate(either.getRight(), issues, objectStack, accessorStack); + } else { + issues.add( + new MessageIssue( + "An Either instance must not be empty." + + " Path: " + + createPathString(accessorStack), + ResponseErrorCode.InvalidParams.getValue())); + } + } else { + for (Method method : object.getClass().getMethods()) { + if (isGetter(method)) { + accessorStack.push(method); + Object value = method.invoke(object); + if (value == null && method.getAnnotation(NonNull.class) != null) { + issues.add( + new MessageIssue( + "The accessor '" + + method.getDeclaringClass().getSimpleName() + + "." + + method.getName() + + "()' must return a non-null value." + + " Path: " + + createPathString(accessorStack), + ResponseErrorCode.InvalidParams.getValue())); + } + validate(value, issues, objectStack, accessorStack); + accessorStack.pop(); + } + } + } + objectStack.pop(); + } + + protected String createPathString(Deque accessorStack) { + StringBuilder result = new StringBuilder("$"); + Iterator resultIter = accessorStack.descendingIterator(); + while (resultIter.hasNext()) { + Object accessor = resultIter.next(); + if (accessor instanceof Method) result.append('.').append(getPropertyName((Method) accessor)); + else if (accessor instanceof Integer) result.append('[').append(accessor).append(']'); + else result.append(accessor); + } + return result.toString(); + } + + protected boolean isGetter(Method method) { + return method.getParameterCount() == 0 + && method.getName().startsWith("get") + && method.getDeclaringClass() != Object.class + && Modifier.isPublic(method.getModifiers()) + && !Modifier.isStatic(method.getModifiers()); + } + + protected String getPropertyName(Method method) { + String methodName = method.getName(); + if (methodName.startsWith("get") && methodName.length() > 3) + return methodName.substring(3, 4).toLowerCase() + methodName.substring(4); + else return methodName; + } +} diff --git a/build-tools/tooling-api/build.gradle b/build-tools/tooling-api/build.gradle index 4eff4cdd0..36eb5e699 100644 --- a/build-tools/tooling-api/build.gradle +++ b/build-tools/tooling-api/build.gradle @@ -27,6 +27,7 @@ java { dependencies { implementation project (path: ':build-tools:logger') + api project (path: ':build-tools:jsonrpc') api project (path: ':build-tools:tooling-api-model') api project (path: ':build-tools:tooling-api-events') @@ -34,8 +35,6 @@ dependencies { testImplementation tests.junit testImplementation tests.google_truth - - api common.jsonrpc api google.gson } diff --git a/build-tools/xml-dom/build.gradle b/build-tools/xml-dom/build.gradle index 3425871bc..83ebcbd30 100644 --- a/build-tools/xml-dom/build.gradle +++ b/build-tools/xml-dom/build.gradle @@ -29,7 +29,6 @@ dependencies { api files ('libs/org.eclipse.lemminx-0.21.0.jar') api "org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0" - implementation common.jsonrpc implementation "com.google.guava:guava:31.1-jre" implementation "com.google.code.gson:gson:2.8.9" implementation "xerces:xercesImpl:2.12.2" diff --git a/settings.gradle b/settings.gradle index 8df61a0cc..3752b4da8 100755 --- a/settings.gradle +++ b/settings.gradle @@ -28,3 +28,4 @@ include ':build-tools:xml-dom' include ':build-tools:xml-formatter' include ':build-tools:fuzzysearch' include ':build-tools:builder-model-impl' +include ':build-tools:jsonrpc'