From 7c3cce32a907ef9d7670f1d9b8e3c194f5d653c0 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Tue, 28 May 2019 12:11:51 -0700 Subject: [PATCH] Make spring-boot-ls request batched classpath events ... ...and make it handle them properly, but also retain support for non-batched events for backwards compatibility with clients that don't support batched events. --- .../java/ls/ClasspathListenerManager.java | 44 ++++++++--- .../SendClasspathNotificationsJob.java | 73 ++++++++++++++----- 2 files changed, 87 insertions(+), 30 deletions(-) diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/ls/ClasspathListenerManager.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/ls/ClasspathListenerManager.java index 97a03fa1c..2ff412b9a 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/ls/ClasspathListenerManager.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/ls/ClasspathListenerManager.java @@ -29,6 +29,7 @@ import org.springframework.ide.vscode.commons.util.AsyncRunner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import reactor.core.Disposable; @@ -57,19 +58,38 @@ public class ClasspathListenerManager { log.debug("callback {} received {}", callbackCommandId, callbackParams); List args = callbackParams.getArguments(); log.debug("args = {}", args); - //Note: not sure... but args might be deserialized as com.google.gson.JsonElement's. - //If so the code below is not correct (casts will fail). - String projectUri = ((JsonElement) args.get(0)).getAsString(); - log.debug("projectUri = {}", args); - String name = ((JsonElement) args.get(1)).getAsString(); - log.debug("name = {}", args); - boolean deleted = ((JsonElement)args.get(2)).getAsBoolean(); - log.debug("deleted = {}", deleted); + //Args are deserialized as com.google.gson.JsonElements. + if (((JsonElement) args.get(0)).isJsonArray()) { + // If events are batched... then they will arrive as a array of arrays. + for (Object arg : args) { + JsonArray event = (JsonArray) arg; - Classpath classpath = gson.fromJson((JsonElement)args.get(3), Classpath.class); - log.debug("classpath = {}", classpath); + String projectUri = event.get(0).getAsString(); + log.debug("projectUri = {}", event); + String name = event.get(1).getAsString(); + log.debug("name = {}", event); + boolean deleted = event.get(2).getAsBoolean(); + log.debug("deleted = {}", deleted); - classpathListener.changed(new ClasspathListener.Event(projectUri, name, deleted, classpath)); + Classpath classpath = gson.fromJson((JsonElement)event.get(3), Classpath.class); + log.debug("classpath = {}", classpath); + classpathListener.changed(new ClasspathListener.Event(projectUri, name, deleted, classpath)); + } + } else { + //Still support non-batched events for backwards compatibility with clients + // that don't provide batched event support (e.g. IDEA client may only adopt this + // later, or not adopt it at all). + String projectUri = ((JsonElement) args.get(0)).getAsString(); + log.debug("projectUri = {}", args); + String name = ((JsonElement) args.get(1)).getAsString(); + log.debug("name = {}", args); + boolean deleted = ((JsonElement)args.get(2)).getAsBoolean(); + log.debug("deleted = {}", deleted); + + Classpath classpath = gson.fromJson((JsonElement)args.get(3), Classpath.class); + log.debug("classpath = {}", classpath); + classpathListener.changed(new ClasspathListener.Event(projectUri, name, deleted, classpath)); + } return "done"; })); @@ -86,7 +106,7 @@ public class ClasspathListenerManager { server.getClient().registerCapability(params) )); Mono registerClasspathListener = Mono.defer(() -> Mono.fromFuture( - server.getClient().addClasspathListener(new ClasspathListenerParams(callbackCommandId)) + server.getClient().addClasspathListener(new ClasspathListenerParams(callbackCommandId, true)) )); Disposable cleanups = () -> { diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/SendClasspathNotificationsJob.java b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/SendClasspathNotificationsJob.java index 94c10af73..9ba26da2a 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/SendClasspathNotificationsJob.java +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/SendClasspathNotificationsJob.java @@ -12,7 +12,9 @@ package org.springframework.tooling.jdt.ls.commons.classpath; import java.io.File; import java.net.URI; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; @@ -26,20 +28,19 @@ import org.eclipse.jdt.core.JavaCore; import org.springframework.ide.vscode.commons.protocol.java.Classpath; import org.springframework.tooling.jdt.ls.commons.Logger; +import com.google.common.collect.ImmutableList; + public class SendClasspathNotificationsJob extends Job { private final ClientCommandExecutor conn; private final Logger logger; private String callbackCommandId; - private boolean isBatched; - - public SendClasspathNotificationsJob(Logger logger, ClientCommandExecutor conn, String callbackId, boolean isBatched) { - super("Send Classpath Notifications"); - this.logger = logger; - this.conn = conn; - this.callbackCommandId = callbackId; - this.isBatched = isBatched; - } + + /** + * Used only if caller has requested 'batched' events. This buffer, accumulates messsages to be sent out all at once, + * rather than one by one. + */ + private List buffer; /** * To keep track of project locations. Without this we can't properly handle deletion events because @@ -47,6 +48,18 @@ public class SendClasspathNotificationsJob extends Job { * was deleted' event if we keep track of project locations ourselves. */ private Map projectLocations = new HashMap<>(); + public final Queue queue = new ConcurrentLinkedQueue<>(); + + public SendClasspathNotificationsJob(Logger logger, ClientCommandExecutor conn, String callbackId, boolean isBatched) { + super("Send Classpath Notifications"); + this.logger = logger; + this.conn = conn; + this.callbackCommandId = callbackId; + if (isBatched) { + buffer = new ArrayList<>(); + } + } + private URI getProjectLocation(IJavaProject jp) { URI loc = jp.getProject().getLocationURI(); @@ -60,7 +73,6 @@ public class SendClasspathNotificationsJob extends Job { } } - public final Queue queue = new ConcurrentLinkedQueue<>(); private boolean projectExists(IJavaProject jp) { //We can't really deal with projects that don't exist in disk. So using this more strict 'exists' check @@ -114,20 +126,45 @@ public class SendClasspathNotificationsJob extends Job { logger.log(e); } } - try { - logger.log("executing callback "+callbackCommandId+" "+projectName+" "+deleted+" "+ classpath.getEntries().size()); - Object r = conn.executeClientCommand(callbackCommandId, projectLoc.toString(), projectName, deleted, classpath); - logger.log("executing callback "+callbackCommandId+" SUCCESS ["+r+"]"); - } catch (Exception e) { - logger.log("executing callback "+callbackCommandId+" FAILED"); - logger.log(e); - } + bufferMessage(projectLoc, deleted, projectName, classpath); } } + flush(); } catch (Exception e) { logger.log(e); } return Status.OK_STATUS; } } + + protected void bufferMessage(URI projectLoc, boolean deleted, String projectName, Classpath classpath) { + if (buffer!=null) { + logger.log("buffering callback "+callbackCommandId+" "+projectName+" "+deleted+" "+ classpath.getEntries().size()); + buffer.add(ImmutableList.of(projectLoc.toString(), projectName, deleted, classpath)); + } else { + try { + logger.log("executing callback "+callbackCommandId+" "+projectName+" "+deleted+" "+ classpath.getEntries().size()); + Object r = conn.executeClientCommand(callbackCommandId, projectLoc.toString(), projectName, deleted, classpath); + logger.log("executing callback "+callbackCommandId+" SUCCESS ["+r+"]"); + } catch (Exception e) { + logger.log("executing callback "+callbackCommandId+" FAILED"); + logger.log(e); + } + } + } + + protected void flush() { + if (buffer!=null && !buffer.isEmpty()) { + try { + logger.log("executing callback "+callbackCommandId+" "+buffer.size()+" batched events"); + Object r = conn.executeClientCommand(callbackCommandId, buffer.toArray(new Object[buffer.size()])); + logger.log("executing callback "+callbackCommandId+" SUCCESS ["+r+"]"); + } catch (Exception e) { + logger.log("executing callback "+callbackCommandId+" FAILED"); + logger.log(e); + } finally { + buffer.clear(); + } + } + } }