| /****************************************************************************** |
| * 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); |
| } |
| |
| } |