Include JSONRpc as a module project

This commit is contained in:
Akash Yadav
2022-05-17 21:59:34 +05:30
parent 5e3756e297
commit 1ec196d006
62 changed files with 6176 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
/build
+30
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
plugins {
id 'java-library'
}
java {
sourceCompatibility = rootProject.ext.javaSourceVersion
targetCompatibility = rootProject.ext.javaTargetVersion
}
dependencies {
implementation google.gson
implementation project (path: ':build-tools:logger')
}
@@ -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;
}
}
@@ -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 <R> CompletableFuture<R> computeAsync(Function<CancelChecker, R> code) {
CompletableFuture<CancelChecker> start = new CompletableFuture<>();
CompletableFuture<R> 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 <R> CompletableFuture<R> computeAsync(Executor executor, Function<CancelChecker, R> code) {
CompletableFuture<CancelChecker> start = new CompletableFuture<>();
CompletableFuture<R> 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();
}
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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 <T> remote service interface type
*/
public interface Launcher<T> {
/**
* 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 <T> Launcher<T> createLauncher(Object localService, Class<T> remoteInterface, InputStream in, OutputStream out) {
return new Builder<T>()
.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 <T> Launcher<T> createLauncher(Object localService, Class<T> remoteInterface, InputStream in, OutputStream out,
boolean validate, PrintWriter trace) {
return new Builder<T>()
.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 <T> Launcher<T> createLauncher(Object localService, Class<T> remoteInterface, InputStream in, OutputStream out,
ExecutorService executorService, Function<MessageConsumer, MessageConsumer> 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 <T> Launcher<T> createIoLauncher(Object localService, Class<T> remoteInterface, InputStream in, OutputStream out,
ExecutorService executorService, Function<MessageConsumer, MessageConsumer> wrapper) {
return new Builder<T>()
.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 <T> Launcher<T> createIoLauncher(Object localService, Class<T> remoteInterface, InputStream in, OutputStream out,
ExecutorService executorService, Function<MessageConsumer, MessageConsumer> wrapper, Consumer<GsonBuilder> configureGson) {
return new Builder<T>()
.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 <T> Launcher<T> createIoLauncher(Object localService, Class<T> remoteInterface, InputStream in, OutputStream out, boolean validate,
ExecutorService executorService, Function<MessageConsumer, MessageConsumer> wrapper, Consumer<GsonBuilder> configureGson) {
return new Builder<T>()
.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<Object> createIoLauncher(Collection<Object> localServices, Collection<Class<?>> remoteInterfaces, ClassLoader classLoader,
InputStream in, OutputStream out, ExecutorService executorService, Function<MessageConsumer, MessageConsumer> wrapper,
Consumer<GsonBuilder> configureGson) {
return new Builder<Object>()
.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 <T> remote service interface type
*/
public static class Builder<T> {
protected Collection<Object> localServices;
protected Collection<Class<? extends T>> remoteInterfaces;
protected InputStream input;
protected OutputStream output;
protected ExecutorService executorService;
protected Function<MessageConsumer, MessageConsumer> messageWrapper;
protected Function<Throwable, ResponseError> exceptionHandler;
protected boolean validateMessages;
protected Consumer<GsonBuilder> configureGson;
protected ClassLoader classLoader;
protected MessageTracer messageTracer;
public Builder<T> setLocalService(Object localService) {
this.localServices = Collections.singletonList(localService);
return this;
}
public Builder<T> setLocalServices(Collection<Object> localServices) {
this.localServices = localServices;
return this;
}
public Builder<T> setRemoteInterface(Class<? extends T> remoteInterface) {
this.remoteInterfaces = Collections.singletonList(remoteInterface);
return this;
}
public Builder<T> setRemoteInterfaces(Collection<Class<? extends T>> remoteInterfaces) {
this.remoteInterfaces = remoteInterfaces;
return this;
}
public Builder<T> setInput(InputStream input) {
this.input = input;
return this;
}
public Builder<T> setOutput(OutputStream output) {
this.output = output;
return this;
}
public Builder<T> setExecutorService(ExecutorService executorService) {
this.executorService = executorService;
return this;
}
public Builder<T> setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
return this;
}
public Builder<T> wrapMessages(Function<MessageConsumer, MessageConsumer> wrapper) {
this.messageWrapper = wrapper;
return this;
}
public Builder<T> setExceptionHandler(Function<Throwable, ResponseError> exceptionHandler) {
this.exceptionHandler = exceptionHandler;
return this;
}
public Builder<T> validateMessages(boolean validate) {
this.validateMessages = validate;
return this;
}
public Builder<T> traceMessages(PrintWriter tracer) {
if (tracer != null) {
this.messageTracer = new MessageTracer(tracer);
}
return this;
}
public Builder<T> configureGson(Consumer<GsonBuilder> configureGson) {
this.configureGson = configureGson;
return this;
}
public Launcher<T> 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<String, JsonRpcMethod> 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<Class<?>>) (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<T> createLauncher(ExecutorService execService, T remoteProxy, RemoteEndpoint remoteEndpoint,
ConcurrentMessageProcessor msgProcessor) {
return new StandardLauncher<T>(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<String, JsonRpcMethod> getSupportedMethods() {
Map<String, JsonRpcMethod> 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<Void> 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();
}
@@ -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;
}
@@ -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<MessageIssue> issues;
public MessageIssueException(Message rpcMessage, MessageIssue issue) {
this.rpcMessage = rpcMessage;
this.issues = Collections.singletonList(issue);
}
public MessageIssueException(Message rpcMessage, Collection<MessageIssue> issues) {
this.rpcMessage = rpcMessage;
this.issues = Collections.unmodifiableList(new ArrayList<>(issues));
}
public MessageIssueException(Message rpcMessage, Stream<MessageIssue> 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<MessageIssue> getIssues() {
return issues;
}
}
@@ -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<MessageIssue> issues);
}
@@ -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;
}
@@ -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<MessageConsumer, MessageConsumer> {
private final PrintWriter printWriter;
private final Map<String, RequestMetadata> sentRequests = new HashMap<>();
private final Map<String, RequestMetadata> 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());
}
}
@@ -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<Throwable, ResponseError> 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<Throwable, ResponseError> exceptionHandler;
private final AtomicInteger nextRequestId = new AtomicInteger();
private final Map<String, PendingRequestInfo> sentRequestMap = new LinkedHashMap<>();
private final Map<String, CompletableFuture<?>> 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<Throwable, ResponseError> 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<Object> request(String method, Object parameter) {
final RequestMessage requestMessage = createRequestMessage(method, parameter);
final CompletableFuture<Object> result =
new CompletableFuture<Object>() {
@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<String, Number> 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<MessageIssue> 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<MessageIssue> 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<MessageIssue> 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<MessageIssue> 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<Object> future;
PendingRequestInfo(RequestMessage requestMessage2, CompletableFuture<Object> future2) {
this.requestMessage = requestMessage2;
this.future = future2;
}
}
}
@@ -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;
}
}
@@ -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<T> implements Launcher<T> {
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<Void> startListening() {
return msgProcessor.beginProcessing(execService);
}
@Override
public T getRemoteProxy() {
return remoteProxy;
}
@Override
public RemoteEndpoint getRemoteEndpoint() {
return remoteEndpoint;
}
}
@@ -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<String, RequestMetadata> sentRequests;
private final Map<String, RequestMetadata> 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<String, RequestMetadata> sentRequests,
Map<String, RequestMetadata> 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 <code>null</code> to use
* default locale.
*/
public TracingMessageConsumer(
MessageConsumer messageConsumer,
Map<String, RequestMetadata> sentRequests,
Map<String, RequestMetadata> 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);
}
}
}
@@ -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<Void> 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<Void> wrapFuture(Future<?> result, MessageProducer messageProducer) {
return new Future<Void>() {
@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<Void> 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;
}
}
@@ -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();
}
}
@@ -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<String, JsonRpcMethod> supportedMethods();
}
@@ -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";
}
@@ -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<String, JsonRpcMethod> supportedMethods;
private MethodProvider methodProvider;
/**
* @param supportedMethods - a map used to resolve RPC methods in {@link #getJsonRpcMethod(String)}
*/
public MessageJsonHandler(Map<String, JsonRpcMethod> 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<String, JsonRpcMethod> supportedMethods, Consumer<GsonBuilder> 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);
}
}
@@ -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);
}
@@ -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();
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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<E> extends TypeAdapter<Collection<E>> {
public static class Factory implements TypeAdapterFactory {
@Override
@SuppressWarnings({ "unchecked" })
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> 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<Collection<Object>> constructor = getConstructor((Class<Collection<Object>>) typeToken.getRawType());
return (TypeAdapter<T>) create(gson, elementType, elementTypeAdapter, constructor);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected TypeAdapter<?> create(Gson gson, Type elementType, TypeAdapter<?> elementTypeAdapter, Supplier<Collection<Object>> constructor) {
return new CollectionTypeAdapter(gson, elementType, elementTypeAdapter, constructor);
}
protected <E> Supplier<Collection<E>> getConstructor(Class<? extends Collection<E>> rawType) {
try {
Constructor<? extends Collection<E>> 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<E>();
else if (Set.class.isAssignableFrom(rawType))
return () -> new LinkedHashSet<E>();
else if (Queue.class.isAssignableFrom(rawType))
return () -> new LinkedList<E>();
else
return () -> new ArrayList<E>();
}
}
}
private final Gson gson;
private final Type elementType;
private final TypeAdapter<E> elementTypeAdapter;
private final Supplier<Collection<E>> constructor;
public CollectionTypeAdapter(Gson gson, Type elementType, TypeAdapter<E> elementTypeAdapter, Supplier<Collection<E>> constructor) {
this.gson = gson;
this.elementType = elementType;
this.elementTypeAdapter = elementTypeAdapter;
this.constructor = constructor;
}
@Override
public Collection<E> read(JsonReader in) throws IOException {
JsonToken peek = in.peek();
if (peek == JsonToken.NULL) {
in.nextNull();
return null;
} else if (peek == JsonToken.BEGIN_ARRAY) {
Collection<E> collection = constructor.get();
in.beginArray();
while (in.hasNext()) {
E instance = elementTypeAdapter.read(in);
collection.add(instance);
}
in.endArray();
return collection;
} else {
Collection<E> collection = constructor.get();
E instance = elementTypeAdapter.read(in);
collection.add(instance);
return collection;
}
}
@Override
public void write(JsonWriter out, Collection<E> 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<E> runtimeTypeAdapter = (TypeAdapter<E>) gson.getAdapter(TypeToken.get(element.getClass()));
runtimeTypeAdapter.write(out, element);
} else {
elementTypeAdapter.write(out, element);
}
}
out.endArray();
}
}
@@ -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<E> extends CollectionTypeAdapter<E> {
public Adapter(Gson gson, Type elementType, TypeAdapter<E> elementTypeAdapter,
Supplier<Collection<E>> constructor) {
super(gson, elementType, elementTypeAdapter, constructor);
}
}
}
@@ -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<L, R> extends TypeAdapter<Either<L, R>> {
public static class Factory implements TypeAdapterFactory {
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> 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<JsonElement> {
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<JsonElement> {
private final Predicate<JsonElement> elementChecker;
private final boolean resultIfEmpty;
public ListChecker(Predicate<JsonElement> elementChecker) {
this(elementChecker, false);
}
public ListChecker(Predicate<JsonElement> 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<Either<L, R>> typeToken;
protected final EitherTypeArgument<L> left;
protected final EitherTypeArgument<R> right;
protected final Predicate<JsonElement> leftChecker;
protected final Predicate<JsonElement> rightChecker;
public EitherTypeAdapter(Gson gson, TypeToken<Either<L, R>> typeToken) {
this(gson, typeToken, null, null);
}
public EitherTypeAdapter(Gson gson, TypeToken<Either<L, R>> typeToken, Predicate<JsonElement> leftChecker, Predicate<JsonElement> rightChecker) {
this.typeToken = typeToken;
Type[] elementTypes = TypeUtils.getElementTypes(typeToken, Either.class);
this.left = new EitherTypeArgument<L>(gson, elementTypes[0]);
this.right = new EitherTypeArgument<R>(gson, elementTypes[1]);
this.leftChecker = leftChecker;
this.rightChecker = rightChecker;
}
@Override
public void write(JsonWriter out, Either<L, R> 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<L, R> read(JsonReader in) throws IOException {
JsonToken next = in.peek();
if (next == JsonToken.NULL) {
in.nextNull();
return null;
}
return create(next, in);
}
protected Either<L, R> 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<L, R> createLeft(L obj) throws IOException {
if (Either3.class.isAssignableFrom(typeToken.getRawType()))
return (Either<L, R>) Either3.forLeft3(obj);
return Either.forLeft(obj);
}
@SuppressWarnings("unchecked")
protected Either<L, R> createRight(R obj) throws IOException {
if (Either3.class.isAssignableFrom(typeToken.getRawType()))
return (Either<L, R>) Either3.forRight3((Either<?, ?>) obj);
return Either.forRight(obj);
}
protected static class EitherTypeArgument<T> {
protected final TypeToken<T> typeToken;
protected final TypeAdapter<T> adapter;
protected final Collection<JsonToken> expectedTokens;
@SuppressWarnings("unchecked")
public EitherTypeArgument(Gson gson, Type type) {
this.typeToken = (TypeToken<T>) TypeToken.get(type);
this.adapter = (type == Object.class) ? (TypeAdapter<T>) 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();
}
}
}
@@ -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<L, R> extends EitherTypeAdapter<L, R> {
public Adapter(Gson gson, TypeToken<Either<L, R>> typeToken) {
super(gson, typeToken);
}
}
}
@@ -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<T extends Enum<T>> extends TypeAdapter<T> {
public static class Factory implements TypeAdapterFactory {
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> 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<String, T> nameToConstant = new HashMap<>();
private final Map<Integer, T> valueToConstant = new HashMap<>();
private final Map<T, Integer> constantToValue = new HashMap<>();
EnumTypeAdapter(Class<T> 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);
}
}
@@ -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 {
}
@@ -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<Object> {
/**
* 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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
return (TypeAdapter<T>) new JsonElementTypeAdapter(gson);
}
}
private final Gson gson;
private final TypeAdapter<JsonElement> 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);;
}
}
}
@@ -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<Message> {
public static class Factory implements TypeAdapterFactory {
private final MessageJsonHandler handler;
public Factory(MessageJsonHandler handler) {
this.handler = handler;
}
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (!Message.class.isAssignableFrom(typeToken.getRawType()))
return null;
return (TypeAdapter<T>) 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<String, Number> 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<Object> parameters = new ArrayList<Object>(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<Object> parameters = new ArrayList<Object>(Math.max(array.size(), parameterTypes.length));
int index = 0;
Iterator<JsonElement> 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<String, Number> 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<String, Number> 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);
}
}
@@ -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);
}
}
@@ -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<Throwable> {
public static class Factory implements TypeAdapterFactory {
@SuppressWarnings({ "unchecked" })
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (!Throwable.class.isAssignableFrom(typeToken.getRawType()))
return null;
return (TypeAdapter<T>) new ThrowableTypeAdapter((TypeToken<Throwable>) typeToken);
}
}
private final TypeToken<Throwable> typeToken;
public ThrowableTypeAdapter(TypeToken<Throwable> 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<Throwable> constructor;
if (message == null && cause == null) {
constructor = (Constructor<Throwable>) typeToken.getRawType().getDeclaredConstructor();
return constructor.newInstance();
} else if (message == null) {
constructor = (Constructor<Throwable>) typeToken.getRawType().getDeclaredConstructor(Throwable.class);
return constructor.newInstance(cause);
} else if (cause == null) {
constructor = (Constructor<Throwable>) typeToken.getRawType().getDeclaredConstructor(String.class);
return constructor.newInstance(message);
} else {
constructor = (Constructor<Throwable>) 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;
}
}
@@ -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<F extends Object, S extends Object> extends TypeAdapter<Two<F, S>> {
protected final TypeAdapter<F> first;
protected final TypeAdapter<S> second;
@SuppressWarnings("unchecked")
public TwoTypeAdapter(Gson gson, TypeToken<Two<F, S>> typeToken) {
Type[] elementTypes = TypeUtils.getElementTypes(typeToken, Two.class);
this.first = gson.getAdapter((TypeToken<F>) TypeToken.get(elementTypes[0]));
this.second = gson.getAdapter((TypeToken<S>) TypeToken.get(elementTypes[1]));
}
@Override
public void write(final JsonWriter out, final Two<F, S> 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<F, S> 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<F, S> result = new Two<F, S>(f, s);
in.endArray();
return result;
}
}
public static class TwoTypeAdapterFactory implements TypeAdapterFactory {
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (!TypeUtils.isTwoTuple(typeToken.getType())) {
return null;
}
return new TwoTypeAdapter(gson, typeToken);
}
}
}
@@ -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<String, Type> 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 <T> Map<String, Type> createVariableMapping(Type type, Class<T> rawType) {
if (type instanceof ParameterizedType) {
TypeVariable<Class<T>>[] vars = rawType.getTypeParameters();
Type[] args = ((ParameterizedType) type).getActualTypeArguments();
Map<String, Type> 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<String, Type> 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 <em>not</em> resolved
* by this method (use {@link #getElementTypes(TypeToken, Class)} to get resolved parameters).
*/
public static Collection<Type> getExpectedTypes(Type type) {
Collection<Type> result = new ArrayList<>();
collectExpectedTypes(type, result);
return result;
}
private static void collectExpectedTypes(Type type, Collection<Type> 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;
}
}
@@ -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<String, Number> id;
@NonNull
public String getId() {
if (id == null)
return null;
if (id.isLeft())
return id.getLeft();
if (id.isRight())
return id.getRight().toString();
return null;
}
@NonNull
public Either<String, Number> getRawId() {
return id;
}
public void setId(@NonNull String id) {
this.id = Either.forLeft(id);
}
public void setId(@NonNull int id) {
this.id = Either.forRight(id);
}
public void setRawId(@NonNull Either<String, Number> id) {
this.id = id;
}
@Override
public String toString() {
return MessageJsonHandler.toString(this);
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CancelParams other = (CancelParams) obj;
if (this.id == null) {
if (other.id != null)
return false;
} else if (!this.id.equals(other.id))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
return result;
}
}
@@ -0,0 +1,223 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Function;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
/**
* An either type maps union types in protocol specifications.
*/
public class Either<L, R> {
public static <L, R> Either<L, R> forLeft(@NonNull L left) {
return new Either<>(left, null);
}
public static <L, R> Either<L, R> forRight(@NonNull R right) {
return new Either<>(null, right);
}
private final L left;
private final R right;
protected Either(L left, R right) {
super();
this.left = left;
this.right = right;
}
public L getLeft() {
return left;
}
public R getRight() {
return right;
}
public Object get() {
if (left != null)
return left;
if (right != null)
return right;
return null;
}
public boolean isLeft() {
return left != null;
}
public boolean isRight() {
return right != null;
}
public <T> T map(
@NonNull Function<? super L, ? extends T> mapLeft,
@NonNull Function<? super R, ? extends T> mapRight) {
if (isLeft()) {
return mapLeft.apply(getLeft());
}
if (isRight()) {
return mapRight.apply(getRight());
}
return null;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Either<?, ?>) {
Either<?, ?> other = (Either<?, ?>) obj;
return (this.left == other.left && this.right == other.right)
|| (this.left != null && other.left != null && this.left.equals(other.left))
|| (this.right != null && other.right != null && this.right.equals(other.right));
}
return false;
}
@Override
public int hashCode() {
if (this.left != null)
return this.left.hashCode();
if (this.right != null)
return this.right.hashCode();
return 0;
}
public String toString() {
StringBuilder builder = new StringBuilder("Either [").append(System.lineSeparator());
builder.append(" left = ").append(left).append(System.lineSeparator());
builder.append(" right = ").append(right).append(System.lineSeparator());
return builder.append("]").toString();
}
/**
* Return a left disjoint type if the given type is either.
*
* @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#getElementTypes(Type, Class, Class)} instead
*/
@Deprecated
public static Type getLeftDisjointType(Type type) {
if (isEither(type)) {
if (type instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getActualTypeArguments()[0];
}
if (type instanceof Class) {
final Class<?> cls = (Class<?>) type;
return cls.getTypeParameters()[0];
}
}
return null;
}
/**
* Return a right disjoint type if the given type is either.
*
* @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#getElementTypes(Type, Class, Class)} instead
*/
@Deprecated
public static Type getRightDisjointType(Type type) {
if (isEither(type)) {
if (type instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getActualTypeArguments()[1];
}
if (type instanceof Class) {
final Class<?> cls = (Class<?>) type;
return cls.getTypeParameters()[1];
}
}
return null;
}
/**
* Return all disjoint types.
*
* @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#getExpectedTypes(Type)} instead
*/
@Deprecated
public static Collection<Type> getAllDisjoinTypes(Type type) {
return collectDisjoinTypes(type, new ArrayList<>());
}
@Deprecated
protected static Collection<Type> collectDisjoinTypes(Type type, Collection<Type> types) {
if (isEither(type)) {
if (type instanceof ParameterizedType) {
return collectDisjoinTypes((ParameterizedType) type, types);
}
if (type instanceof Class) {
return collectDisjoinTypes((Class<?>) type, types);
}
}
types.add(type);
return types;
}
@Deprecated
protected static Collection<Type> collectDisjoinTypes(ParameterizedType type, Collection<Type> types) {
for (Type typeArgument : type.getActualTypeArguments()) {
collectDisjoinTypes(typeArgument, types);
}
return types;
}
@Deprecated
protected static Collection<Type> collectDisjoinTypes(Class<?> type, Collection<Type> types) {
for (Type typeParameter : type.getTypeParameters()) {
collectDisjoinTypes(typeParameter, types);
}
return types;
}
/**
* Test whether the given type is Either.
*
* @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#isEither(Type)} instead
*/
@Deprecated
public static boolean isEither(Type type) {
if (type instanceof ParameterizedType) {
return isEither((ParameterizedType) type);
}
if (type instanceof Class) {
return isEither((Class<?>) type);
}
return false;
}
/**
* Test whether the given type is Either.
*
* @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#isEither(Type)} instead
*/
@Deprecated
public static boolean isEither(ParameterizedType type) {
return isEither(type.getRawType());
}
/**
* Test whether the given class is Either.
*
* @deprecated Use {@link org.eclipse.lsp4j.jsonrpc.json.adapters.TypeUtils#isEither(Type)} instead
*/
@Deprecated
public static boolean isEither(Class<?> cls) {
return Either.class.isAssignableFrom(cls);
}
}
@@ -0,0 +1,111 @@
/******************************************************************************
* Copyright (c) 2017 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
import java.util.function.Function;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
/**
* Union type for three types.
*/
public class Either3<T1, T2, T3> extends Either<T1, Either<T2, T3>> {
public static <T1, T2, T3> Either3<T1, T2, T3> forFirst(@NonNull T1 first) {
return new Either3<T1, T2, T3>(first, null);
}
public static <T1, T2, T3> Either3<T1, T2, T3> forSecond(@NonNull T2 second) {
return new Either3<T1, T2, T3>(null, new Either<T2, T3>(second, null));
}
public static <T1, T2, T3> Either3<T1, T2, T3> forThird(@NonNull T3 third) {
return new Either3<T1, T2, T3>(null, new Either<T2, T3>(null, third));
}
public static <T1, T2, T3> Either3<T1, T2, T3> forLeft3(@NonNull T1 first) {
return new Either3<T1, T2, T3>(first, null);
}
public static <T1, T2, T3> Either3<T1, T2, T3> forRight3(@NonNull Either<T2, T3> right) {
return new Either3<T1, T2, T3>(null, right);
}
protected Either3(T1 left, Either<T2, T3> right) {
super(left, right);
}
public T1 getFirst() {
return getLeft();
}
public T2 getSecond() {
Either<T2, T3> right = getRight();
if (right == null)
return null;
else
return right.getLeft();
}
public T3 getThird() {
Either<T2, T3> right = getRight();
if (right == null)
return null;
else
return right.getRight();
}
@Override
public Object get() {
if (isRight())
return getRight().get();
return super.get();
}
public boolean isFirst() {
return isLeft();
}
public boolean isSecond() {
return isRight() && getRight().isLeft();
}
public boolean isThird() {
return isRight() && getRight().isRight();
}
public <T> T map(
@NonNull Function<? super T1, ? extends T> mapFirst,
@NonNull Function<? super T2, ? extends T> mapSecond,
@NonNull Function<? super T3, ? extends T> mapThird) {
if (isFirst()) {
return mapFirst.apply(getFirst());
}
if (isSecond()) {
return mapSecond.apply(getSecond());
}
if (isThird()) {
return mapThird.apply(getThird());
}
return null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("Either3 [").append(System.lineSeparator());
builder.append(" first = ").append(getFirst()).append(System.lineSeparator());
builder.append(" second = ").append(getSecond()).append(System.lineSeparator());
builder.append(" third = ").append(getThird()).append(System.lineSeparator());
return builder.append("]").toString();
}
}
@@ -0,0 +1,81 @@
/******************************************************************************
* Copyright (c) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
/**
* A message with an {@code id} property.
*/
public abstract class IdentifiableMessage extends Message {
/**
* The message identifier.
*/
@NonNull
private Either<String, Number> id;
public String getId() {
if (id == null)
return null;
if (id.isLeft())
return id.getLeft();
if (id.isRight())
return id.getRight().toString();
return null;
}
@NonNull
public Either<String, Number> getRawId() {
return id;
}
public void setId(String id) {
this.id = Either.forLeft(id);
}
public void setId(int id) {
this.id = Either.forRight(id);
}
public void setRawId(@NonNull Either<String, Number> id) {
this.id = id;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
if (!super.equals(obj))
return false;
IdentifiableMessage other = (IdentifiableMessage) obj;
if (this.id == null) {
if (other.id != null)
return false;
} else if (!this.id.equals(other.id))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
return result;
}
}
@@ -0,0 +1,66 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
import org.eclipse.lsp4j.jsonrpc.json.MessageConstants;
import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
/**
* A general message as defined by JSON-RPC. The language server protocol always
* uses "2.0" as the jsonrpc version.
*/
public abstract class Message {
@NonNull
private String jsonrpc = MessageConstants.JSONRPC_VERSION;
@NonNull
public String getJsonrpc() {
return this.jsonrpc;
}
public void setJsonrpc(@NonNull String jsonrpc) {
this.jsonrpc = jsonrpc;
}
@Override
public String toString() {
return MessageJsonHandler.toString(this);
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Message other = (Message) obj;
if (this.jsonrpc == null) {
if (other.jsonrpc != null)
return false;
} else if (!this.jsonrpc.equals(other.jsonrpc))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.jsonrpc == null) ? 0 : this.jsonrpc.hashCode());
return result;
}
}
@@ -0,0 +1,60 @@
/******************************************************************************
* Copyright (c) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
/**
* Describes an issue found while parsing or validating a message.
*/
public class MessageIssue {
@NonNull
private String text;
private int code;
private Exception cause;
public MessageIssue(@NonNull String text) {
this(text, 0, null);
}
public MessageIssue(@NonNull String text, int code) {
this(text, code, null);
}
public MessageIssue(@NonNull String text, int code, Exception cause) {
this.text = text;
this.code = code;
this.cause = cause;
}
@NonNull
public String getText() {
return text;
}
public int getIssueCode() {
return code;
}
public Exception getCause() {
return cause;
}
@Override
public String toString() {
return getText();
}
}
@@ -0,0 +1,83 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
/**
* A notification message. A processed notification message must not send a
* response back. They work like events.
*/
public class NotificationMessage extends Message {
/**
* The method to be invoked.
*/
@NonNull
private String method;
@NonNull
public String getMethod() {
return this.method;
}
public void setMethod(@NonNull String method) {
this.method = method;
}
/**
* The method's params.
*/
private Object params;
public Object getParams() {
return this.params;
}
public void setParams(Object params) {
this.params = params;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
if (!super.equals(obj))
return false;
NotificationMessage other = (NotificationMessage) obj;
if (this.method == null) {
if (other.method != null)
return false;
} else if (!this.method.equals(other.method))
return false;
if (this.params == null) {
if (other.params != null)
return false;
} else if (!this.params.equals(other.params))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((this.method == null) ? 0 : this.method.hashCode());
result = prime * result + ((this.params == null) ? 0 : this.params.hashCode());
return result;
}
}
@@ -0,0 +1,84 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
/**
* A request message to describe a request between the client and the server.
* Every processed request must send a response back to the sender of the
* request.
*/
public class RequestMessage extends IdentifiableMessage {
/**
* The method to be invoked.
*/
@NonNull
private String method;
@NonNull
public String getMethod() {
return this.method;
}
public void setMethod(@NonNull String method) {
this.method = method;
}
/**
* The method's parameters. The object type depends on the chosen method.
*/
private Object params;
public Object getParams() {
return this.params;
}
public void setParams(Object params) {
this.params = params;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
if (!super.equals(obj))
return false;
RequestMessage other = (RequestMessage) obj;
if (this.method == null) {
if (other.method != null)
return false;
} else if (!this.method.equals(other.method))
return false;
if (this.params == null) {
if (other.params != null)
return false;
} else if (!this.params.equals(other.params))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((this.method == null) ? 0 : this.method.hashCode());
result = prime * result + ((this.params == null) ? 0 : this.params.hashCode());
return result;
}
}
@@ -0,0 +1,123 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
import org.eclipse.lsp4j.jsonrpc.json.adapters.JsonElementTypeAdapter;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
import com.google.gson.annotations.JsonAdapter;
public class ResponseError {
/**
* A number indicating the error type that occurred.
*/
@NonNull
private int code;
@NonNull
public int getCode() {
return this.code;
}
public void setCode(@NonNull int code) {
this.code = code;
}
public void setCode(ResponseErrorCode code) {
this.setCode(code.getValue());
}
/**
* A string providing a short description of the error.
*/
@NonNull
private String message;
@NonNull
public String getMessage() {
return this.message;
}
public void setMessage(@NonNull String message) {
this.message = message;
}
/**
* A Primitive or Structured value that contains additional information
* about the error. Can be omitted.
*/
@JsonAdapter(JsonElementTypeAdapter.Factory.class)
private Object data;
public Object getData() {
return this.data;
}
public void setData(Object data) {
this.data = data;
}
public ResponseError() {
}
public ResponseError(ResponseErrorCode code, String message, Object data) {
this(code.getValue(), message, data);
}
public ResponseError(int code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
@Override
public String toString() {
return MessageJsonHandler.toString(this);
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ResponseError other = (ResponseError) obj;
if (other.code != this.code)
return false;
if (this.message == null) {
if (other.message != null)
return false;
} else if (!this.message.equals(other.message))
return false;
if (this.data == null) {
if (other.data != null)
return false;
} else if (!this.data.equals(other.data))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.code;
result = prime * result + ((this.message == null) ? 0 : this.message.hashCode());
result = prime * result + ((this.data == null) ? 0 : this.data.hashCode());
return result;
}
}
@@ -0,0 +1,129 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
/**
* A number indicating the error type that occurred.
*/
public enum ResponseErrorCode {
ParseError(-32700),
InvalidRequest(-32600),
MethodNotFound(-32601),
InvalidParams(-32602),
InternalError(-32603),
/**
* This is the start range of JSON RPC reserved error codes.
* It doesn't denote a real error code. No LSP error codes should
* be defined between the start and end range. For backwards
* compatibility the {@link #serverNotInitialized} and the
* {@link #UnknownErrorCode} are left in the range.
* <p>
* Since 3.16.0
*/
jsonrpcReservedErrorRangeStart(-32099),
/** @deprecated use {@link #jsonrpcReservedErrorRangeStart} */
@Deprecated
serverErrorStart(-32099),
/**
* Error code indicating that a server received a notification or
* request before the server has received the {@code initialize} request.
* <p>
* Should be {@code ServerNotInitialized}
*/
serverNotInitialized(-32002),
UnknownErrorCode(-32001),
/**
* This is the end range of JSON RPC reserved error codes.
* It doesn't denote a real error code.
* <p>
* Since 3.16.0
*/
jsonrpcReservedErrorRangeEnd(-32000),
/** @deprecated use {@link #jsonrpcReservedErrorRangeEnd} */
@Deprecated
serverErrorEnd(-32000),
/**
* This is the start range of LSP reserved error codes.
* It doesn't denote a real error code.
* <p>
* Since 3.16.0
*/
lspReservedErrorRangeStart(-32899),
/**
* A request failed but it was syntactically correct, e.g the
* method name was known and the parameters were valid. The error
* message should contain human readable information about why
* the request failed.
* <p>
* Since 3.17.0
*/
RequestFailed(-32803),
/**
* The server cancelled the request. This error code should
* only be used for requests that explicitly support being
* server cancellable.
* <p>
* Since 3.17.0
*/
ServerCancelled(-32802),
/**
* The server detected that the content of a document got
* modified outside normal conditions. A server should
* NOT send this error code if it detects a content change
* in it unprocessed messages. The result even computed
* on an older state might still be useful for the client.
* <p>
* If a client decides that a result is not of any use anymore
* the client should cancel the request.
*/
ContentModified(-32801),
/**
* The client has canceled a request and a server as detected
* the cancel.
*/
RequestCancelled(-32800),
/**
* This is the end range of LSP reserved error codes.
* It doesn't denote a real error code.
* <p>
* Since 3.16.0
*/
lspReservedErrorRangeEnd(-32800);
private final int value;
ResponseErrorCode(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
@@ -0,0 +1,83 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.messages;
/**
* Response message sent as a result of a request. If a request doesn't provide
* a result value the receiver of a request still needs to return a response
* message to conform to the JSON RPC specification. The result property of the
* ResponseMessage should be set to null in this case to signal a successful
* request. A response message is linked to a request via their {@code id} properties.
*/
public class ResponseMessage extends IdentifiableMessage {
/**
* The result of a request. This can be omitted in the case of an error.
* The object type depends on the method of the corresponding request.
*/
private Object result;
public Object getResult() {
return this.result;
}
public void setResult(Object result) {
this.result = result;
}
/**
* The error object in case a request fails.
*/
private ResponseError error;
public ResponseError getError() {
return this.error;
}
public void setError(ResponseError error) {
this.error = error;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
if (!super.equals(obj))
return false;
ResponseMessage other = (ResponseMessage) obj;
if (this.result == null) {
if (other.result != null)
return false;
} else if (!this.result.equals(other.result))
return false;
if (this.error == null) {
if (other.error != null)
return false;
} else if (!this.error.equals(other.error))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((this.result == null) ? 0 : this.result.hashCode());
result = prime * result + ((this.error == null) ? 0 : this.error.hashCode());
return result;
}
}
@@ -0,0 +1,81 @@
package org.eclipse.lsp4j.jsonrpc.messages;
import java.io.IOException;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* Representation of tuple types.
*/
@SuppressWarnings("all")
public interface Tuple {
public static <F, S> Two<F, S> two(F first, S second) {
return new Two<F, S>(first, second);
}
/**
* A two-tuple, i.e. a pair.
*/
public static class Two<F extends Object, S extends Object> implements Tuple {
private final F first;
private final S second;
public Two(F first, S second) {
this.first = first;
this.second = second;
}
public F getFirst() {
return this.first;
}
public S getSecond() {
return this.second;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Tuple.Two<?, ?> other = (Tuple.Two<?, ?>) obj;
if (this.first == null) {
if (other.first != null)
return false;
} else if (!this.first.equals(other.first))
return false;
if (this.second == null) {
if (other.second != null)
return false;
} else if (!this.second.equals(other.second))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.first == null) ? 0 : this.first.hashCode());
return prime * result + ((this.second == null) ? 0 : this.second.hashCode());
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("Tuples.Two [").append(System.lineSeparator());
builder.append(" first = ").append(first).append(System.lineSeparator());
builder.append(" second = ").append(second).append(System.lineSeparator());
return builder.append("]").toString();
}
}
}
@@ -0,0 +1,131 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Set;
import java.util.function.Consumer;
public final class AnnotationUtil {
private AnnotationUtil() {}
public static void findDelegateSegments(Class<?> clazz, Set<Class<?>> visited, Consumer<Method> acceptor) {
if (clazz == null || !visited.add(clazz))
return;
findDelegateSegments(clazz.getSuperclass(), visited, acceptor);
for (Class<?> interf : clazz.getInterfaces()) {
findDelegateSegments(interf, visited, acceptor);
}
for (Method method : clazz.getDeclaredMethods()) {
if (isDelegateMethod(method)) {
acceptor.accept(method);
}
}
}
public static boolean isDelegateMethod(Method method) {
if (!method.isSynthetic()) {
JsonDelegate jsonDelegate = method.getAnnotation(JsonDelegate.class);
if (jsonDelegate != null) {
if (!(method.getParameterCount() == 0 && method.getReturnType().isInterface())) {
throw new IllegalStateException(
"The method " + method.toString() + " is not a proper @JsonDelegate method.");
}
return true;
}
}
return false;
}
/**
* Depth first search for annotated methods in hierarchy.
*/
public static void findRpcMethods(Class<?> clazz, Set<Class<?>> visited, Consumer<MethodInfo> acceptor) {
if (clazz == null || !visited.add(clazz))
return;
findRpcMethods(clazz.getSuperclass(), visited, acceptor);
for (Class<?> interf : clazz.getInterfaces()) {
findRpcMethods(interf, visited, acceptor);
}
String segment = getSegment(clazz);
for (Method method : clazz.getDeclaredMethods()) {
MethodInfo methodInfo = createMethodInfo(method, segment);
if (methodInfo != null) {
acceptor.accept(methodInfo);
}
}
}
protected static String getSegment(Class<?> clazz) {
JsonSegment jsonSegment = clazz.getAnnotation(JsonSegment.class);
return jsonSegment == null ? "" : jsonSegment.value() + "/";
}
protected static MethodInfo createMethodInfo(Method method, String segment) {
if (!method.isSynthetic()) {
JsonRequest jsonRequest = method.getAnnotation(JsonRequest.class);
if (jsonRequest != null) {
return createRequestInfo(method, segment, jsonRequest);
}
JsonNotification jsonNotification = method.getAnnotation(JsonNotification.class);
if (jsonNotification != null) {
return createNotificationInfo(method, segment, jsonNotification);
}
}
return null;
}
protected static MethodInfo createNotificationInfo(Method method, String segment, JsonNotification jsonNotification) {
MethodInfo methodInfo = createMethodInfo(method, jsonNotification.useSegment(), segment, jsonNotification.value());
methodInfo.isNotification = true;
return methodInfo;
}
protected static MethodInfo createRequestInfo(Method method, String segment, JsonRequest jsonRequest) {
return createMethodInfo(method, jsonRequest.useSegment(), segment, jsonRequest.value());
}
protected static MethodInfo createMethodInfo(Method method, boolean useSegment, String segment, String value) {
method.setAccessible(true);
MethodInfo methodInfo = new MethodInfo();
methodInfo.method = method;
methodInfo.parameterTypes = getParameterTypes(method);
methodInfo.name = getMethodName(method, useSegment, segment, value);
return methodInfo;
}
protected static String getMethodName(Method method, boolean useSegment, String segment, String value) {
String name = value != null && value.length() > 0 ? value : method.getName();
return useSegment ? segment + name : name;
}
protected static Type[] getParameterTypes(Method method) {
return Arrays.stream(method.getParameters()).map(t -> t.getParameterizedType()).toArray(Type[]::new);
}
static class MethodInfo {
private static Type[] EMPTY_TYPE_ARRAY = {};
public String name;
public Method method;
public Type[] parameterTypes = EMPTY_TYPE_ARRAY;
public boolean isNotification = false;
}
static class DelegateInfo {
public Method method;
public Object delegate;
}
}
@@ -0,0 +1,130 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import org.eclipse.lsp4j.jsonrpc.Endpoint;
import org.eclipse.lsp4j.jsonrpc.services.AnnotationUtil.DelegateInfo;
import org.eclipse.lsp4j.jsonrpc.services.AnnotationUtil.MethodInfo;
/**
* A Proxy that wraps an {@link Endpoint} in one or more service interfaces, i.e. interfaces
* containing {@link JsonNotification} and {@link JsonRequest} methods.
*/
public class EndpointProxy implements InvocationHandler {
private final Method object_equals;
private final Method object_hashCode;
private final Method object_toString;
private final Endpoint delegate;
private final LinkedHashMap<String, MethodInfo> methodInfos;
private final LinkedHashMap<String, DelegateInfo> delegatedSegments;
public EndpointProxy(Endpoint delegate, Class<?> interface_) {
this(delegate, Collections.singletonList(interface_));
}
public EndpointProxy(Endpoint delegate, Collection<Class<?>> interfaces) {
if (delegate == null)
throw new NullPointerException("delegate");
if (interfaces == null)
throw new NullPointerException("interfaces");
if (interfaces.isEmpty())
throw new IllegalArgumentException("interfaces must not be empty.");
this.delegate = delegate;
try {
object_equals = Object.class.getDeclaredMethod("equals", Object.class);
object_hashCode = Object.class.getDeclaredMethod("hashCode");
object_toString = Object.class.getDeclaredMethod("toString");
} catch (NoSuchMethodException | SecurityException exception) {
throw new RuntimeException(exception);
}
methodInfos = new LinkedHashMap<>();
delegatedSegments = new LinkedHashMap<>();
for (Class<?> interf : interfaces) {
AnnotationUtil.findRpcMethods(interf, new HashSet<Class<?>>(), (methodInfo) -> {
if (methodInfos.put(methodInfo.method.getName(), methodInfo) != null) {
throw new IllegalStateException("Duplicate RPC method " + methodInfo.method);
}
});
AnnotationUtil.findDelegateSegments(interf, new HashSet<Class<?>>(), (method) -> {
Object delegateProxy = ServiceEndpoints.toServiceObject(delegate, method.getReturnType());
DelegateInfo info = new DelegateInfo();
info.delegate = delegateProxy;
info.method = method;
if (delegatedSegments.put(method.getName(), info) != null) {
throw new IllegalStateException("Duplicate RPC method " + method);
}
});
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
args = args == null ? new Object[0] : args;
MethodInfo methodInfo = this.methodInfos.get(method.getName());
if (methodInfo != null) {
Object params = getParams(args, methodInfo);
if (methodInfo.isNotification) {
delegate.notify(methodInfo.name, params);
return null;
}
return delegate.request(methodInfo.name, params);
}
DelegateInfo delegateInfo = this.delegatedSegments.get(method.getName());
if (delegateInfo != null) {
return delegateInfo.delegate;
}
if (object_equals.equals(method) && args.length == 1) {
if(args[0] != null ) {
try {
return this.equals(Proxy.getInvocationHandler(args[0]));
} catch (IllegalArgumentException exception) {
}
}
return this.equals(args[0]);
}
if (object_hashCode.equals(method)) {
return this.hashCode();
}
if (object_toString.equals(method)) {
return this.toString();
}
return method.invoke(delegate, args);
}
protected Object getParams(Object[] args, MethodInfo methodInfo) {
if (args.length == 0) {
return null;
}
if (args.length == 1) {
return args[0];
}
return Arrays.asList(args);
}
@Override
public String toString() {
return getClass().getSimpleName() + " for " + delegate.toString();
}
}
@@ -0,0 +1,190 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;
import com.itsaky.androidide.utils.ILogger;
import org.eclipse.lsp4j.jsonrpc.Endpoint;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* An endpoint that reflectively delegates to {@link JsonNotification} and {@link JsonRequest}
* methods of one or more given delegate objects.
*/
public class GenericEndpoint implements Endpoint {
private static final ILogger LOG = ILogger.newInstance("GenericEndpoint");
private static final Object[] NO_ARGUMENTS = {};
private final LinkedHashMap<String, Function<Object, CompletableFuture<Object>>> methodHandlers =
new LinkedHashMap<>();
private final List<Object> delegates;
public GenericEndpoint(Object delegate) {
this.delegates = Collections.singletonList(delegate);
recursiveFindRpcMethods(delegate, new HashSet<>(), new HashSet<>());
}
public GenericEndpoint(Collection<Object> delegates) {
this.delegates = new ArrayList<>(delegates);
for (Object delegate : this.delegates) {
recursiveFindRpcMethods(delegate, new HashSet<>(), new HashSet<>());
}
}
@Override
public CompletableFuture<?> request(String method, Object parameter) {
// Check the registered method handlers
Function<Object, CompletableFuture<Object>> handler = methodHandlers.get(method);
if (handler != null) {
return handler.apply(parameter);
}
// Ask the delegate objects whether they can handle the request generically
List<CompletableFuture<?>> futures = new ArrayList<>(delegates.size());
for (Object delegate : delegates) {
if (delegate instanceof Endpoint) {
futures.add(((Endpoint) delegate).request(method, parameter));
}
}
if (!futures.isEmpty()) {
return CompletableFuture.anyOf(futures.toArray(new CompletableFuture[futures.size()]));
}
// Create a log message about the unsupported method
String message = "Unsupported request method: " + method;
if (isOptionalMethod(method)) {
LOG.info(message);
return CompletableFuture.completedFuture(null);
}
LOG.warn(message);
CompletableFuture<?> exceptionalResult = new CompletableFuture<Object>();
ResponseError error = new ResponseError(ResponseErrorCode.MethodNotFound, message, null);
exceptionalResult.completeExceptionally(new ResponseErrorException(error));
return exceptionalResult;
}
@Override
public void notify(String method, Object parameter) {
// Check the registered method handlers
Function<Object, CompletableFuture<Object>> handler = methodHandlers.get(method);
if (handler != null) {
handler.apply(parameter);
return;
}
// Ask the delegate objects whether they can handle the notification generically
int notifiedDelegates = 0;
for (Object delegate : delegates) {
if (delegate instanceof Endpoint) {
((Endpoint) delegate).notify(method, parameter);
notifiedDelegates++;
}
}
if (notifiedDelegates == 0) {
// Create a log message about the unsupported method
String message = "Unsupported notification method: " + method;
if (isOptionalMethod(method)) {
LOG.info(message);
} else {
LOG.warn(message);
}
}
}
protected boolean isOptionalMethod(String method) {
return method != null && method.startsWith("$/");
}
protected void recursiveFindRpcMethods(
Object current, Set<Class<?>> visited, Set<Class<?>> visitedForDelegate) {
AnnotationUtil.findRpcMethods(
current.getClass(),
visited,
(methodInfo) -> {
@SuppressWarnings("unchecked")
Function<Object, CompletableFuture<Object>> handler =
(arg) -> {
try {
Method method = methodInfo.method;
Object[] arguments = this.getArguments(method, arg);
return (CompletableFuture<Object>) method.invoke(current, arguments);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
};
if (methodHandlers.put(methodInfo.name, handler) != null) {
throw new IllegalStateException("Multiple methods for name " + methodInfo.name);
}
});
AnnotationUtil.findDelegateSegments(
current.getClass(),
visitedForDelegate,
(method) -> {
try {
Object delegate = method.invoke(current);
if (delegate != null) {
recursiveFindRpcMethods(delegate, visited, visitedForDelegate);
} else {
LOG.error(
"A delegate object is null, jsonrpc methods of '" + method + "' are ignored");
}
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
protected Object[] getArguments(Method method, Object arg) {
int parameterCount = method.getParameterCount();
if (parameterCount == 0) {
if (arg != null) {
LOG.warn("Unexpected params '" + arg + "' for '" + method + "' is ignored");
}
return NO_ARGUMENTS;
}
if (arg instanceof List<?>) {
List<?> arguments = (List<?>) arg;
int argumentCount = arguments.size();
if (argumentCount == parameterCount) {
return arguments.toArray();
}
if (argumentCount > parameterCount) {
Stream<?> unexpectedArguments = arguments.stream().skip(parameterCount);
String unexpectedParams =
unexpectedArguments.map(a -> "'" + a + "'").reduce((a, a2) -> a + ", " + a2).get();
LOG.warn("Unexpected params " + unexpectedParams + " for '" + method + "' is ignored");
return arguments.subList(0, parameterCount).toArray();
}
return arguments.toArray(new Object[parameterCount]);
}
Object[] arguments = new Object[parameterCount];
arguments[0] = arg;
return arguments;
}
}
@@ -0,0 +1,28 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A method annotated with {@link JsonDelegate} is treated as a delegate method.
* As a result jsonrpc methods of the delegate will be considered, too.
* If an annotated method returns `null`
* then jsonrpc methods of the delegate are not considered.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface JsonDelegate {
}
@@ -0,0 +1,52 @@
/******************************************************************************
* Copyright (c) 2016, 2020 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to mark a notification method on an interface or class.
* <p>
* A notification method must be of type <code>void</code> and have zero or one
* argument.
* <p>
* According to jsonrpc an argument must be an 'object' (a java bean, not e,g.
* String).
* <p>
* The name of the jsonrpc notification will be the optional segment, followed
* by the name of the Java method that is annotated with JsonNotification. The
* name of the jsonrpc notification can be customized by using the
* {@link #value()} field of this annotation. To specify the whole name,
* including the segment, in the value, set {@link #useSegment()} to false.
*
* @see JsonSegment
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface JsonNotification {
/**
* The name of the the jsonrpc request method. If empty, uses the name of the
* annotated method.
*/
String value() default "";
/**
* When using segments, useSegment will be true to prepend the segment name to
* the name of the request.
*
* @see JsonSegment
*/
boolean useSegment() default true;
}
@@ -0,0 +1,53 @@
/******************************************************************************
* Copyright (c) 2016, 2020 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.CompletableFuture;
/**
* Annotation to mark a request method on an interface or class.
* <p>
* A request method must have the return type {@link CompletableFuture} with an
* object parameter type or Void and have zero or one argument.
* <p>
* According to jsonrpc an argument must be an 'object' (a java bean, not e,g.
* String).
* <p>
* The name of the jsonrpc request will be the optional segment, followed by the
* name of the Java method that is annotated with JsonRequest. The name of the
* jsonrpc request can be customized by using the {@link #value()} field of this
* annotation. To specify the whole name, including the segment, in the value,
* set {@link #useSegment()} to false.
*
* @see JsonSegment
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface JsonRequest {
/**
* The name of the the jsonrpc request method. If empty, uses the name of the
* annotated method.
*/
String value() default "";
/**
* When using segments, useSegment will be true to prepend the segment name to
* the name of the request.
*
* @see JsonSegment
*/
boolean useSegment() default true;
}
@@ -0,0 +1,26 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Use on a class or interface to prefix all declared {@link JsonRequest} and {@link JsonNotification} methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JsonSegment {
String value() default "";
}
@@ -0,0 +1,128 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.lsp4j.jsonrpc.Endpoint;
import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod;
import org.eclipse.lsp4j.jsonrpc.json.ResponseJsonAdapter;
import com.google.gson.TypeAdapterFactory;
public final class ServiceEndpoints {
private ServiceEndpoints() {}
/**
* Wraps a given {@link Endpoint} in the given service interface.
*
* @return the wrapped service object
*/
@SuppressWarnings("unchecked")
public static <T> T toServiceObject(Endpoint endpoint, Class<T> interface_) {
Class<?>[] interfArray = new Class[]{interface_, Endpoint.class};
EndpointProxy invocationHandler = new EndpointProxy(endpoint, interface_);
return (T) Proxy.newProxyInstance(interface_.getClassLoader(), interfArray, invocationHandler);
}
/**
* Wraps a given {@link Endpoint} in the given service interfaces.
*
* @return the wrapped service object
*/
public static Object toServiceObject(Endpoint endpoint, Collection<Class<?>> interfaces, ClassLoader classLoader) {
Class<?>[] interfArray = new Class[interfaces.size() + 1];
interfaces.toArray(interfArray);
interfArray[interfArray.length - 1] = Endpoint.class;
EndpointProxy invocationHandler = new EndpointProxy(endpoint, interfaces);
return Proxy.newProxyInstance(classLoader, interfArray, invocationHandler);
}
/**
* Wraps a given object with service annotations behind an {@link Endpoint} interface.
*
* @return the wrapped service endpoint
*/
public static Endpoint toEndpoint(Object serviceObject) {
return new GenericEndpoint(serviceObject);
}
/**
* Wraps a collection of objects with service annotations behind an {@link Endpoint} interface.
*
* @return the wrapped service endpoint
*/
public static Endpoint toEndpoint(Collection<Object> serviceObjects) {
return new GenericEndpoint(serviceObjects);
}
/**
* Finds all Json RPC methods on a given class.
*
* @return the supported JsonRpcMethods
*/
public static Map<String, JsonRpcMethod> getSupportedMethods(Class<?> type) {
Set<Class<?>> visitedTypes = new HashSet<>();
return getSupportedMethods(type, visitedTypes);
}
/**
* Finds all Json RPC methods on a given type
*/
private static Map<String, JsonRpcMethod> getSupportedMethods(Class<?> type, Set<Class<?>> visitedTypes) {
Map<String, JsonRpcMethod> result = new LinkedHashMap<String, JsonRpcMethod>();
AnnotationUtil.findRpcMethods(type, visitedTypes, (methodInfo) -> {
JsonRpcMethod meth;
if (methodInfo.isNotification) {
meth = JsonRpcMethod.notification(methodInfo.name, methodInfo.parameterTypes);
} else {
Type genericReturnType = methodInfo.method.getGenericReturnType();
if (genericReturnType instanceof ParameterizedType) {
Type returnType = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
TypeAdapterFactory responseTypeAdapter = null;
ResponseJsonAdapter responseTypeAdapterAnnotation = methodInfo.method.getAnnotation(ResponseJsonAdapter.class);
if (responseTypeAdapterAnnotation != null) {
try {
responseTypeAdapter = responseTypeAdapterAnnotation.value().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
meth = JsonRpcMethod.request(methodInfo.name, returnType, responseTypeAdapter, methodInfo.parameterTypes);
} else {
throw new IllegalStateException("Expecting return type of CompletableFuture but was : " + genericReturnType);
}
}
if (result.put(methodInfo.name, meth) != null) {
throw new IllegalStateException("Duplicate RPC method "+methodInfo.name+".");
};
});
AnnotationUtil.findDelegateSegments(type, new HashSet<>(), (method)-> {
Map<String, JsonRpcMethod> supportedDelegateMethods = getSupportedMethods(method.getReturnType(), visitedTypes);
for (JsonRpcMethod meth : supportedDelegateMethods.values()) {
if (result.put(meth.getMethodName(), meth) != null) {
throw new IllegalStateException("Duplicate RPC method "+meth.getMethodName()+".");
};
}
});
return result;
}
}
@@ -0,0 +1,31 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.validation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated field or parameter must not be set to {@code null}. If validation
* is active, an exception is thrown when a message is received where a {@code NonNull} field
* has a {@code null} or {@code undefined} value.
*
* In order to achieve consistent behavior, for every field with this annotation the corresponding
* getter method as well as the parameter of the corresponding setter method are also expected to
* be annotated.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_PARAMETER})
public @interface NonNull {
}
@@ -0,0 +1,195 @@
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.validation;
import com.google.gson.JsonElement;
import com.itsaky.androidide.utils.ILogger;
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
import org.eclipse.lsp4j.jsonrpc.MessageIssueException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Message;
import org.eclipse.lsp4j.jsonrpc.messages.MessageIssue;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
/**
* Validates messages and forwards them to other message consumers. In case an issue is found, a
* {@link MessageIssueException} is thrown.
*/
public class ReflectiveMessageValidator implements MessageConsumer {
private static final ILogger LOG = ILogger.newInstance("ReflectiveMessageValidator");
private final MessageConsumer delegate;
/** When created with this constructor, the validator acts as a message sink. */
public ReflectiveMessageValidator() {
this.delegate = null;
}
/** Forward messages to the given consumer unless an issue is found. */
public ReflectiveMessageValidator(MessageConsumer delegate) {
this.delegate = delegate;
}
@Override
public void consume(Message message) throws MessageIssueException, JsonRpcException {
List<MessageIssue> issues = validate(message);
if (!issues.isEmpty()) {
// Sort the messages in order to get a stable order (otherwise it depends on the JVM's
// reflection implementation)
Collections.sort(issues, (issue1, issue2) -> issue1.getText().compareTo(issue2.getText()));
throw new MessageIssueException(message, issues);
} else if (delegate != null) {
delegate.consume(message);
}
}
/** Check whether the given object is valid. If it is not valid, its issues are not reported. */
public boolean isValid(Object object) {
List<MessageIssue> issues = validate(object);
return issues.isEmpty();
}
protected List<MessageIssue> validate(Object object) {
List<MessageIssue> result = new ArrayList<>();
try {
validate(object, result, new LinkedList<>(), new LinkedList<>());
} catch (Exception e) {
LOG.error("Error during message validation: " + e.getMessage(), e);
result.add(
new MessageIssue(
"Message validation failed, please check the logs of the remote endpoint.",
ResponseErrorCode.InvalidParams.getValue()));
}
return result;
}
/** Validate all fields of the given object. */
protected void validate(
Object object,
List<MessageIssue> issues,
Deque<Object> objectStack,
Deque<Object> accessorStack)
throws Exception {
if (object == null
|| object instanceof Enum<?>
|| object instanceof String
|| object instanceof Number
|| object instanceof Boolean
|| object instanceof JsonElement
|| object instanceof Throwable) {
return;
}
if (objectStack.contains(object)) {
issues.add(
new MessageIssue(
"An element of the message has a direct or indirect reference to itself."
+ " Path: "
+ createPathString(accessorStack),
ResponseErrorCode.InvalidParams.getValue()));
return;
}
objectStack.push(object);
if (object instanceof List<?>) {
ListIterator<?> iter = ((List<?>) object).listIterator();
while (iter.hasNext()) {
accessorStack.push(iter.nextIndex());
Object element = iter.next();
if (element == null) {
issues.add(
new MessageIssue(
"Lists must not contain null references."
+ " Path: "
+ createPathString(accessorStack),
ResponseErrorCode.InvalidParams.getValue()));
}
validate(element, issues, objectStack, accessorStack);
accessorStack.pop();
}
} else if (object instanceof Either<?, ?>) {
Either<?, ?> either = (Either<?, ?>) object;
if (either.isLeft()) {
validate(either.getLeft(), issues, objectStack, accessorStack);
} else if (either.isRight()) {
validate(either.getRight(), issues, objectStack, accessorStack);
} else {
issues.add(
new MessageIssue(
"An Either instance must not be empty."
+ " Path: "
+ createPathString(accessorStack),
ResponseErrorCode.InvalidParams.getValue()));
}
} else {
for (Method method : object.getClass().getMethods()) {
if (isGetter(method)) {
accessorStack.push(method);
Object value = method.invoke(object);
if (value == null && method.getAnnotation(NonNull.class) != null) {
issues.add(
new MessageIssue(
"The accessor '"
+ method.getDeclaringClass().getSimpleName()
+ "."
+ method.getName()
+ "()' must return a non-null value."
+ " Path: "
+ createPathString(accessorStack),
ResponseErrorCode.InvalidParams.getValue()));
}
validate(value, issues, objectStack, accessorStack);
accessorStack.pop();
}
}
}
objectStack.pop();
}
protected String createPathString(Deque<Object> accessorStack) {
StringBuilder result = new StringBuilder("$");
Iterator<Object> resultIter = accessorStack.descendingIterator();
while (resultIter.hasNext()) {
Object accessor = resultIter.next();
if (accessor instanceof Method) result.append('.').append(getPropertyName((Method) accessor));
else if (accessor instanceof Integer) result.append('[').append(accessor).append(']');
else result.append(accessor);
}
return result.toString();
}
protected boolean isGetter(Method method) {
return method.getParameterCount() == 0
&& method.getName().startsWith("get")
&& method.getDeclaringClass() != Object.class
&& Modifier.isPublic(method.getModifiers())
&& !Modifier.isStatic(method.getModifiers());
}
protected String getPropertyName(Method method) {
String methodName = method.getName();
if (methodName.startsWith("get") && methodName.length() > 3)
return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
else return methodName;
}
}
+1 -2
View File
@@ -27,6 +27,7 @@ java {
dependencies {
implementation project (path: ':build-tools:logger')
api project (path: ':build-tools:jsonrpc')
api project (path: ':build-tools:tooling-api-model')
api project (path: ':build-tools:tooling-api-events')
@@ -34,8 +35,6 @@ dependencies {
testImplementation tests.junit
testImplementation tests.google_truth
api common.jsonrpc
api google.gson
}
-1
View File
@@ -29,7 +29,6 @@ dependencies {
api files ('libs/org.eclipse.lemminx-0.21.0.jar')
api "org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0"
implementation common.jsonrpc
implementation "com.google.guava:guava:31.1-jre"
implementation "com.google.code.gson:gson:2.8.9"
implementation "xerces:xercesImpl:2.12.2"
+1
View File
@@ -28,3 +28,4 @@ include ':build-tools:xml-dom'
include ':build-tools:xml-formatter'
include ':build-tools:fuzzysearch'
include ':build-tools:builder-model-impl'
include ':build-tools:jsonrpc'