Skip to content

Commit

Permalink
fix: instantiate JsonObject with the classloader of the IJ plugin which
Browse files Browse the repository at this point in the history
defines the action

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Dec 6, 2023
1 parent bfc2aa2 commit 500253d
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 80 deletions.
190 changes: 119 additions & 71 deletions src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
******************************************************************************/
package com.redhat.devtools.lsp4ij.commands;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.*;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ActionUtil;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
Expand All @@ -33,20 +31,22 @@
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.services.LanguageServer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;

/**
* This class provides methods to execute {@link Command} instances.
* This class provides methods to execute LSP {@link Command} instances.
*/
public class CommandExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class);
Expand All @@ -63,25 +63,23 @@ public class CommandExecutor {
* the server, nor the client are able to handle the command explicitly, a
* heuristic method will try to interpret the command locally.
*
* @param command
* the LSP Command to be executed. If {@code null} this method will
* do nothing.
* @param documentUri
* the URI of the document for which the command was created
* @param languageServerId
* the ID of the language server for which the {@code command} is
* applicable. If {@code null}, the command will not be executed on
* the language server.
* @param command the LSP Command to be executed. If {@code null} this method will
* do nothing.
* @param documentUri the URI of the document for which the command was created
* @param project the project.
* @param languageServerId the ID of the language server for which the {@code command} is
* applicable. If {@code null}, the command will not be executed on
* the language server.
*/
public static void executeCommand(Project project, Command command, URI documentUri,
String languageServerId) {
public static void executeCommand(@Nullable Command command, @Nullable URI documentUri,
@NotNull Project project, @Nullable String languageServerId) {
if (command == null) {
return;
}
if (executeCommandServerSide(project, command, documentUri, languageServerId)) {
if (executeCommandServerSide(command, documentUri, project, languageServerId)) {
return;
}
if (executeCommandClientSide(project, command, documentUri)) {
if (executeCommandClientSide(command, null, project, null)) {
return;
}
// tentative fallback
Expand All @@ -94,8 +92,19 @@ public static void executeCommand(Project project, Command command, URI document
}
}

private static boolean executeCommandServerSide(Project project, Command command,
URI documentUri, String languageServerId) {
/**
* Execute LSP command on server side.
*
* @param command the LSP Command to be executed. If {@code null} this method will
* do nothing.
* @param documentUri the URI of the document for which the command was created
* @param project the project.
* @param languageServerId the ID of the language server for which the {@code command} is
* applicable.
* @return true if the LSP command on server side has been executed successfully and false otherwise.
*/
private static boolean executeCommandServerSide(@NotNull Command command, @Nullable URI documentUri,
@NotNull Project project, @Nullable String languageServerId) {
if (languageServerId == null) {
return false;
}
Expand Down Expand Up @@ -142,17 +151,28 @@ private static CompletableFuture<LanguageServer> getLanguageServerForCommand(Pro
});
}

private static boolean executeCommandClientSide(Project project, Command command, URI documentUri) {
/**
* Execute LSP command on server side.
*
* @param command the LSP Command to be executed. If {@code null} this method will
* do nothing.
* @param documentUri the URI of the document for which the command was created
* @param project the project.
* @param source the component which has triggered the command and null otherwise.
* @return true if the LSP command on server side has been executed successfully and false otherwise.
*/
public static boolean executeCommandClientSide(@NotNull Command command, @Nullable URI documentUri,
@NotNull Project project, @Nullable Component source) {
Application workbench = ApplicationManager.getApplication();
if (workbench == null) {
return false;
}
AnAction parameterizedCommand = createIDEACoreCommand(command);
if (parameterizedCommand == null) {
AnAction action = createIDEACoreCommand(command);
if (action == null) {
return false;
}
DataContext dataContext = createDataContext(project, command, documentUri);
ActionUtil.invokeAction(parameterizedCommand, dataContext, ActionPlaces.UNKNOWN, null, null);
DataContext dataContext = createDataContext(documentUri, command, action, source, project);
ActionUtil.invokeAction(action, dataContext, ActionPlaces.UNKNOWN, null, null);
return true;
}

Expand All @@ -164,15 +184,44 @@ private static AnAction createIDEACoreCommand(Command command) {
return ActionManager.getInstance().getAction(commandId);
}

private static DataContext createDataContext(Project project, Command command, URI documentUri) {

private static DataContext createDataContext(URI documentUri, Command command, AnAction action, Component source, Project project) {
SimpleDataContext.Builder contextBuilder = SimpleDataContext.builder();
contextBuilder.add(CommonDataKeys.PROJECT, project)
.add(LSP_COMMAND, command)
.add(LSP_COMMAND_DOCUMENT_URI, documentUri);
if (source != null) {
contextBuilder.setParent(DataManager.getInstance().getDataContext(source));
}
ensureArgumentsIsInProperClassloader(command, action.getClass().getClassLoader());
contextBuilder
.add(CommonDataKeys.PROJECT, project)
.add(LSP_COMMAND, command);
if (documentUri != null) {
contextBuilder.add(LSP_COMMAND_DOCUMENT_URI, documentUri);
}
return contextBuilder.build();
}

private static void ensureArgumentsIsInProperClassloader(Command command, ClassLoader classLoader) {
List<Object> arguments = command.getArguments();
if (arguments == null || arguments.isEmpty()) {
return;
}
for (int i = 0; i < arguments.size(); i++) {
Object arg = arguments.get(i);
if (arg instanceof JsonElement elt) {
// At this step, JsonElement is an instance coming from the LSP4IJ plugin class loader.
// If external plugin which consumes LSP4IJ and declare a gson dependency, it will have
// ClasCastException error which command arguments will be used.
// In this case, JsonElement requires to be updated by creating a new JsonElement with the external plugin class loader.
Object newElt = GsonManager.getJsonElementFromClassloader(elt, classLoader);
if (newElt != null) {
arguments.set(i, newElt);
} else {
// Here, the JsonElement class loader should be valid, no need to create new instances of JsonElement.
return;
}
}
}
}

// TODO consider using Entry/SimpleEntry instead
private static final class Pair<K, V> {
K key;
Expand All @@ -185,6 +234,7 @@ private static final class Pair<K, V> {
}

// this method may be turned public if needed elsewhere

/**
* Very empirical and unsafe heuristic to turn unknown command arguments into a
* workspace edit...
Expand All @@ -199,58 +249,56 @@ private static WorkspaceEdit createWorkspaceEdit(List<Object> commandArguments,
if (item instanceof List) {
return ((List<?>) item).stream();
} else {
return Collections.singleton(item).stream();
return Stream.of(item);
}
}).forEach(arg -> {
if (arg instanceof String) {
if (arg instanceof String) {
changes.put(currentEntry.key.toString(), currentEntry.value);
VirtualFile resource = LSPIJUtils.findResourceFor((String) arg);
if (resource != null) {
currentEntry.key = LSPIJUtils.toUri(resource);
currentEntry.value = new ArrayList<>();
}
} else if (arg instanceof WorkspaceEdit) {
changes.putAll(((WorkspaceEdit) arg).getChanges());
} else if (arg instanceof TextEdit) {
currentEntry.value.add((TextEdit) arg);
} else if (arg instanceof Map) {
Gson gson = new Gson(); // TODO? retrieve the GSon used by LS
TextEdit edit = gson.fromJson(gson.toJson(arg), TextEdit.class);
if (edit != null) {
currentEntry.value.add(edit);
}
} else if (arg instanceof JsonPrimitive json) {
if (json.isString()) {
changes.put(currentEntry.key.toString(), currentEntry.value);
VirtualFile resource = LSPIJUtils.findResourceFor((String) arg);
VirtualFile resource = LSPIJUtils.findResourceFor(json.getAsString());
if (resource != null) {
currentEntry.key = LSPIJUtils.toUri(resource);
currentEntry.value = new ArrayList<>();
}
} else if (arg instanceof WorkspaceEdit) {
changes.putAll(((WorkspaceEdit) arg).getChanges());
} else if (arg instanceof TextEdit) {
currentEntry.value.add((TextEdit) arg);
} else if (arg instanceof Map) {
Gson gson = new Gson(); // TODO? retrieve the GSon used by LS
TextEdit edit = gson.fromJson(gson.toJson(arg), TextEdit.class);
}
} else if (arg instanceof JsonArray array) {
Gson gson = new Gson(); // TODO? retrieve the GSon used by LS
array.forEach(elt -> {
TextEdit edit = gson.fromJson(gson.toJson(elt), TextEdit.class);
if (edit != null) {
currentEntry.value.add(edit);
}
} else if (arg instanceof JsonPrimitive) {
JsonPrimitive json = (JsonPrimitive) arg;
if (json.isString()) {
changes.put(currentEntry.key.toString(), currentEntry.value);
VirtualFile resource = LSPIJUtils.findResourceFor(json.getAsString());
if (resource != null) {
currentEntry.key = LSPIJUtils.toUri(resource);
currentEntry.value = new ArrayList<>();
}
}
} else if (arg instanceof JsonArray) {
Gson gson = new Gson(); // TODO? retrieve the GSon used by LS
JsonArray array = (JsonArray) arg;
array.forEach(elt -> {
TextEdit edit = gson.fromJson(gson.toJson(elt), TextEdit.class);
if (edit != null) {
currentEntry.value.add(edit);
}
});
} else if (arg instanceof JsonObject) {
Gson gson = new Gson(); // TODO? retrieve the GSon used by LS
WorkspaceEdit wEdit = gson.fromJson((JsonObject) arg, WorkspaceEdit.class);
Map<String, List<TextEdit>> entries = wEdit.getChanges();
if (wEdit != null && !entries.isEmpty()) {
changes.putAll(entries);
} else {
TextEdit edit = gson.fromJson((JsonObject) arg, TextEdit.class);
if (edit != null && edit.getRange() != null) {
currentEntry.value.add(edit);
}
});
} else if (arg instanceof JsonObject) {
Gson gson = new Gson(); // TODO? retrieve the GSon used by LS
WorkspaceEdit wEdit = gson.fromJson((JsonObject) arg, WorkspaceEdit.class);
Map<String, List<TextEdit>> entries = wEdit.getChanges();
if (wEdit != null && !entries.isEmpty()) {
changes.putAll(entries);
} else {
TextEdit edit = gson.fromJson((JsonObject) arg, TextEdit.class);
if (edit != null && edit.getRange() != null) {
currentEntry.value.add(edit);
}
}
}
});
if (!currentEntry.value.isEmpty()) {
changes.put(currentEntry.key.toString(), currentEntry.value);
Expand Down
67 changes: 67 additions & 0 deletions src/main/java/com/redhat/devtools/lsp4ij/commands/GsonManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*******************************************************************************
* Copyright (c) 2020 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at https://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
* Fraunhofer FOKUS
******************************************************************************/
package com.redhat.devtools.lsp4ij.commands;

import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.intellij.ide.plugins.cl.PluginClassLoader;

import java.lang.reflect.Method;

/**
* Gson manager.
*/
public class GsonManager {

private GsonManager() {

}

/**
* Returns the JsonElement instance with the given class loader and null otherwise.
*
* @param elt the JsonElement with the lsp4ij plugin class loader.
* @param actionClassLoader the IJ action class loader.
* @return the JsonElement instance with the given class loader and null otherwise.
*/
public static Object getJsonElementFromClassloader(JsonElement elt, ClassLoader actionClassLoader) {
if (elt.getClass().getClassLoader() == actionClassLoader || !(actionClassLoader instanceof PluginClassLoader)) {
// - the JsonElement class has the same class loader as the IJ Action class loader
// - or the action class loader is not a PluginClassLoader
// --> do nothing
return null;
}
try {
Class<?> jsonParserClass = ((PluginClassLoader) actionClassLoader).tryLoadingClass(JsonParser.class.getName(), true);
if (jsonParserClass != null) {
try {
// Try to get static method JsonParser#parseString from the new version of Gson
// public static JsonElement parseString(String json) throws JsonSyntaxException {
Method parseString = jsonParserClass.getDeclaredMethod("parseString", String.class);
return parseString.invoke(jsonParserClass, elt.toString());
} catch (Exception e) {
// Old version of Gson
try {
// public JsonElement parse(String json) throws JsonSyntaxException {
Method parse = jsonParserClass.getDeclaredMethod("parse", String.class);
return parse.invoke(jsonParserClass.getDeclaredConstructor().newInstance(), elt.toString());
} catch (Exception e1) {

}
}
}
} catch (Exception e) {

}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,7 @@ public boolean isLanguageSupported(@NotNull Language language) {

protected void executeClientCommand(@NotNull Component source, @NotNull Command command, @NotNull Project project) {
if (command != null) {
AnAction action = ActionManager.getInstance().getAction(command.getCommand());
if (action != null) {
DataContext context = SimpleDataContext.getSimpleContext(CommandExecutor.LSP_COMMAND, command, DataManager.getInstance().getDataContext(source));
action.actionPerformed(new AnActionEvent(null, context,
ActionPlaces.UNKNOWN, new Presentation(),
ActionManager.getInstance(), 0));
}
CommandExecutor.executeCommandClientSide(command, null, project, source);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private void apply(CodeAction codeaction, @NotNull Project project, PsiFile file
}

private void executeCommand(Command command, @NotNull Project project, PsiFile file, String serverId) {
CommandExecutor.executeCommand(project, command, LSPIJUtils.toUri(file), serverId);
CommandExecutor.executeCommand(command, LSPIJUtils.toUri(file), project, serverId);
}

private LanguageServerWrapper getLanguageServerWrapper() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ private void executeCustomCommand(@NotNull Command command, URI documentUri) {
LanguageServiceAccessor.getInstance(project)
.resolveServerDefinition(languageServer.getServer()).map(definition -> definition.id)
.ifPresent(id -> {
CommandExecutor.executeCommand(project, command, documentUri, id);
CommandExecutor.executeCommand(command, documentUri, project, id);
});

}
Expand Down

0 comments on commit 500253d

Please sign in to comment.