blob: 960bacf2c8cf094f36b8c05657f1c8e5a033f1ab [file] [log] [blame]
/******************************************************************************
* Copyright (c) 2017 Kichwa Coders Ltd. 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.debug.adapters;
import java.io.EOFException;
import java.io.IOException;
import java.lang.reflect.Type;
import org.eclipse.lsp4j.jsonrpc.MessageIssueException;
import org.eclipse.lsp4j.jsonrpc.debug.messages.DebugNotificationMessage;
import org.eclipse.lsp4j.jsonrpc.debug.messages.DebugRequestMessage;
import org.eclipse.lsp4j.jsonrpc.debug.messages.DebugResponseMessage;
import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod;
import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
import org.eclipse.lsp4j.jsonrpc.json.MethodProvider;
import org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter;
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.ResponseError;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
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 {@link DebugMessageTypeAdapter} provides an adapter that maps Debug
* Server Protocol style JSON to/from LSP4J's JSONRPC implementation. The Debug
* Server Protocol (DSP) has its own message format that is quite similar to
* JSON-RPC 2.0. The DSP is defined in a <a href=
* "https://github.com/Microsoft/vscode-debugadapter-node/blob/master/debugProtocol.json">JSON
* schema in the VS Code Debug Adapter</a>. This section documents how LSP4J's
* jsonrpc classes maps to the Debug Protocol, using some extensions in the DSP
* code to the lsp4j's {@link Message}s.
* <p>
*
* <pre>
"ProtocolMessage": { // implemented by {@link Message}
"type": "object",
"description": "Base class of requests, responses, and events.",
"properties": {
"seq": { // implemented by (depending on type, with conversion to/from String):
// {@link DebugRequestMessage#getId()}, or
// {@link DebugNotificationMessage#getId()} or
// {@link DebugResponseMessage#getResponseId()}
"type": "integer",
"description": "Sequence number."
},
"type": { // implicit in type of subclass of {@link Message}
"type": "string",
"description": "Message type.",
"_enum": [ "request", "response", "event" ]
}
},
"required": [ "seq", "type" ]
},
"Request": { // implemented by {@link DebugRequestMessage}
"allOf": [ { "$ref": "#/definitions/ProtocolMessage" }, {
"type": "object",
"description": "A client or server-initiated request.",
"properties": {
"type": { // implicit by being of type {@link DebugRequestMessage}
"type": "string",
"enum": [ "request" ]
},
"command": { // implemented by {@link DebugRequestMessage#getMethod()}
"type": "string",
"description": "The command to execute."
},
"arguments": { // implemented by {@link DebugRequestMessage#getParams()}
"type": [ "array", "boolean", "integer", "null", "number" , "object", "string" ],
"description": "Object containing arguments for the command."
}
},
"required": [ "type", "command" ]
}]
},
"Event": { // implemented by {@link DebugNotificationMessage}
"allOf": [ { "$ref": "#/definitions/ProtocolMessage" }, {
"type": "object",
"description": "Server-initiated event.",
"properties": {
"type": { // implicit by being of type {@link DebugNotificationMessage}
"type": "string",
"enum": [ "event" ]
},
"event": { // implemented by {@link DebugNotificationMessage#getMethod()}
"type": "string",
"description": "Type of event."
},
"body": { // implemented by {@link DebugNotificationMessage#getParams()}
"type": [ "array", "boolean", "integer", "null", "number" , "object", "string" ],
"description": "Event-specific information."
}
},
"required": [ "type", "event" ]
}]
},
"Response": { // implemented by {@link DebugResponseMessage}
"allOf": [ { "$ref": "#/definitions/ProtocolMessage" }, {
"type": "object",
"description": "Response to a request.",
"properties": {
"type": { // implicit by being of type {@link DebugResponseMessage}
"type": "string",
"enum": [ "response" ]
},
"request_seq": { // implemented by {@link DebugResponseMessage#getId()}
"type": "integer",
"description": "Sequence number of the corresponding request."
},
"success": { // implemented by {@link DebugResponseMessage#getError()} == null
"type": "boolean",
"description": "Outcome of the request."
},
"command": { // implemented by {@link DebugResponseMessage#getMethod()}
"type": "string",
"description": "The command requested."
},
"message": { // implemented by {@link ResponseError#getMessage()}
"type": "string",
"description": "Contains error message if success == false."
},
"body": { // implemented by {@link DebugResponseMessage#getResult()} for success and {@link ResponseError#getData()} for error
"type": [ "array", "boolean", "integer", "null", "number" , "object", "string" ],
"description": "Contains request result if success is true and optional error details if success is false."
}
},
"required": [ "type", "request_seq", "success", "command" ]
}]
},
* </pre>
*
*/
public class DebugMessageTypeAdapter extends MessageTypeAdapter {
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 DebugMessageTypeAdapter(handler, gson);
}
}
private final MessageJsonHandler handler;
private final Gson gson;
public DebugMessageTypeAdapter(MessageJsonHandler handler, Gson gson) {
super(handler, gson);
this.handler = handler;
this.gson = gson;
}
@Override
public Message read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
in.beginObject();
String messageType = null, method = null, message = null;
int seq = 0, request_seq = 0;
Boolean rawSuccess = null;
Object rawParams = null;
Object rawBody = null;
try {
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "seq": {
seq = in.nextInt();
break;
}
case "request_seq": {
// on responses we treat the request_seq as the id
request_seq = in.nextInt();
break;
}
case "type": {
messageType = in.nextString();
break;
}
case "success": {
rawSuccess = in.nextBoolean();
break;
}
case "command": {
method = in.nextString();
break;
}
case "event": {
method = in.nextString();
break;
}
case "message": {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
} else {
message = in.nextString();
}
break;
}
case "arguments": {
rawParams = parseParams(in, method);
break;
}
case "body": {
rawBody = parseBody(in, messageType, request_seq, method, rawSuccess);
break;
}
default:
in.skipValue();
}
}
boolean success = rawSuccess != null ? rawSuccess : false;
Object params = parseParams(rawParams, method);
Object body = parseBody(rawBody, messageType, request_seq, method, success);
in.endObject();
return createMessage(messageType, seq, request_seq, method, success, message, params, body);
} catch (JsonSyntaxException | MalformedJsonException | EOFException exception) {
if ("request".equals(messageType) || "event".equals(messageType) || "response".equals(messageType)) {
// Create a message and bundle it to an exception with an issue that wraps the original exception
boolean success = rawSuccess != null ? rawSuccess : false;
Message resultMessage = createMessage(messageType, seq, request_seq, method, success, message, rawParams, rawBody);
MessageIssue issue = new MessageIssue("Message could not be parsed.", ResponseErrorCode.ParseError.getValue(), exception);
throw new MessageIssueException(resultMessage, issue);
} else {
throw exception;
}
}
}
/**
* Convert the json input into the body object corresponding to the type of
* message.
*
* If the type of message or any other necessary field is not known until after
* parsing, call {@link #parseBody(Object, String, int, String, Boolean)} on
* the return value of this call for a second chance conversion.
*
* @param in
* json input to read from
* @param messageType
* message type if known
* @param request_seq
* seq id of request message if known
* @param method
* event/method being called
* @param success
* if success of a response is known
* @return correctly typed object if the correct expected type can be
* determined, or a JsonElement representing the body
*/
protected Object parseBody(JsonReader in, String messageType, int request_seq, String method, Boolean success)
throws IOException {
if ("event".equals(messageType)) {
return parseParams(in, method);
} else if ("response".equals(messageType) && success != null && success) {
return super.parseResult(in, Integer.toString(request_seq));
} else {
return JsonParser.parseReader(in);
}
}
/**
* Convert the JsonElement into the body object corresponding to the type of
* message. If the rawBody is already converted, does nothing.
*
* @param rawBody
* json element to read from
* @param messageType
* message type if known
* @param request_seq
* seq id of request message if known
* @param method
* event/method being called
* @param success
* if success of a response is known
* @return correctly typed object if the correct expected type can be
* determined, or rawBody unmodified if no conversion can be done.
*/
protected Object parseBody(Object rawBody, String messageType, int request_seq, String method, Boolean success) {
if ("event".equals(messageType)) {
return parseParams(rawBody, method);
} else if ("response".equals(messageType)) {
if (success != null && success) {
return super.parseResult(rawBody, Integer.toString(request_seq));
}
if (isNull(rawBody)) {
return null;
}
if (!(rawBody instanceof JsonElement)) {
return rawBody;
}
JsonElement rawJsonParams = (JsonElement) rawBody;
return fromJson(rawJsonParams, Object.class);
}
return rawBody;
}
private Message createMessage(String messageType, int seq, int request_seq, String method, boolean success,
String errorMessage, Object params, Object body) throws JsonParseException {
if (messageType == null) {
throw new JsonParseException("Unable to identify the input message. Missing 'type' field.");
}
switch (messageType) {
case "request": {
DebugRequestMessage message = new DebugRequestMessage();
message.setId(seq);
message.setMethod(method);
message.setParams(params);
return message;
}
case "event": {
DebugNotificationMessage message = new DebugNotificationMessage();
message.setId(seq);
message.setMethod(method);
message.setParams(body);
return message;
}
case "response": {
DebugResponseMessage message = new DebugResponseMessage();
message.setId(request_seq);
message.setResponseId(seq);
message.setMethod(method);
if (!success) {
ResponseError error = new ResponseError();
error.setCode(ResponseErrorCode.UnknownErrorCode);
error.setData(body);
if (errorMessage == null) {
// Some debug servers/clients don't provide a "message" field on an error.
// Generally in those cases the body has some extra details to figure out
// what went wrong.
errorMessage = "Unset error message.";
}
error.setMessage(errorMessage);
message.setError(error);
} else {
if (body instanceof JsonElement) {
// Type of result could not be resolved - try again with the parsed JSON tree
MethodProvider methodProvider = handler.getMethodProvider();
if (methodProvider != null) {
String resolvedMethod = methodProvider.resolveMethod(Integer.toString(request_seq));
if (resolvedMethod != null) {
JsonRpcMethod jsonRpcMethod = handler.getJsonRpcMethod(resolvedMethod);
if (jsonRpcMethod != null) {
TypeAdapter<?> typeAdapter = null;
Type returnType = jsonRpcMethod.getReturnType();
if (jsonRpcMethod.getReturnTypeAdapterFactory() != null)
typeAdapter = jsonRpcMethod.getReturnTypeAdapterFactory().create(gson, TypeToken.get(returnType));
JsonElement jsonElement = (JsonElement) body;
if (typeAdapter != null)
body = typeAdapter.fromJsonTree(jsonElement);
else
body = gson.fromJson(jsonElement, returnType);
}
}
}
}
message.setResult(body);
}
return message;
}
default:
throw new JsonParseException("Unable to identify the input message.");
}
}
@Override
public void write(JsonWriter out, Message message) throws IOException {
out.beginObject();
if (message instanceof DebugRequestMessage) {
DebugRequestMessage requestMessage = (DebugRequestMessage) message;
out.name("type");
out.value("request");
out.name("seq");
writeIntId(out, requestMessage.getRawId());
out.name("command");
out.value(requestMessage.getMethod());
Object params = requestMessage.getParams();
if (params != null) {
out.name("arguments");
gson.toJson(params, params.getClass(), out);
}
} else if (message instanceof DebugResponseMessage) {
DebugResponseMessage responseMessage = (DebugResponseMessage) message;
out.name("type");
out.value("response");
out.name("seq");
writeIntId(out, responseMessage.getRawResponseId());
out.name("request_seq");
writeIntId(out, responseMessage.getRawId());
out.name("command");
out.value(responseMessage.getMethod());
ResponseError error = responseMessage.getError();
if (error != null) {
out.name("success");
out.value(false);
String errorMessage = error.getMessage();
out.name("message");
if (errorMessage == null)
writeNullValue(out);
else
gson.toJson(errorMessage, errorMessage.getClass(), out);
Object errorData = error.getData();
if (errorData != null) {
out.name("body");
gson.toJson(errorData, errorData.getClass(), out);
}
} else {
out.name("success");
out.value(true);
Object result = responseMessage.getResult();
if (result != null) {
out.name("body");
gson.toJson(result, result.getClass(), out);
}
}
} else if (message instanceof DebugNotificationMessage) {
DebugNotificationMessage notificationMessage = (DebugNotificationMessage) message;
out.name("type");
out.value("event");
out.name("seq");
writeIntId(out, notificationMessage.getRawId());
out.name("event");
out.value(notificationMessage.getMethod());
Object params = notificationMessage.getParams();
if (params != null) {
out.name("body");
gson.toJson(params, params.getClass(), out);
}
}
out.endObject();
}
private void writeIntId(JsonWriter out, Either<String, Number> id) throws IOException {
if (id == null)
writeNullValue(out);
else if (id.isLeft())
out.value(Integer.parseInt(id.getLeft()));
else if (id.isRight())
out.value(id.getRight());
}
}