blob: b543a739eeb6dec20e64f104bbdc3e3d17152700 [file] [log] [blame]
/******************************************************************************
* 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();
}