blob: bffdecb1ebd446c79b6aa1f707ac302cb7c019d6 [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.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);
}
}