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 extends T> 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 extends TypeAdapterFactory> 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 extends Collection> rawType) {
+ try {
+ Constructor extends Collection> 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 extends JsonElement> 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 extends JsonElement> 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