From fc8ae02145b0b49b5c1536b2c019ebdad1749532 Mon Sep 17 00:00:00 2001 From: Rafael Bey <24432403+rafaelbey@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:51:05 -0500 Subject: [PATCH] Reduce shaded jar size by managing runtime dependencies using maven (#123) * Reduce shaded jar size by managing runtime dependencies using maven * Incorporate review feedback: restrict catch block and explicitly set charset --- .../pom.xml | 4 - .../ConnectionLSPGrammarExtension.java | 50 ++- .../TestConnectionLSPGrammarExtension.java | 3 +- legend-engine-ide-lsp-server-shaded/pom.xml | 20 +- legend-engine-ide-lsp-server/pom.xml | 13 + .../ide/lsp/classpath/ClasspathFactory.java | 23 ++ .../classpath/ClasspathUsingMavenFactory.java | 187 ++++++++++ .../classpath/EmbeddedClasspathFactory.java | 28 ++ .../ide/lsp/server/ExtensionsGuard.java | 120 +++++++ .../ide/lsp/server/LegendLanguageServer.java | 323 ++++++++++-------- .../lsp/server/LegendServerGlobalState.java | 9 + .../lsp/server/LegendTextDocumentService.java | 169 ++++----- .../lsp/server/LegendWorkspaceService.java | 10 + .../engine/ide/lsp/DummyLanguageClient.java | 104 ++++++ .../maven/TestClasspathUsingMavenFactory.java | 66 ++++ .../lsp/server/TestLegendLanguageServer.java | 65 ++-- pom.xml | 31 ++ 17 files changed, 949 insertions(+), 276 deletions(-) create mode 100644 legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/ClasspathFactory.java create mode 100644 legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/ClasspathUsingMavenFactory.java create mode 100644 legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/EmbeddedClasspathFactory.java create mode 100644 legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/ExtensionsGuard.java create mode 100644 legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/DummyLanguageClient.java create mode 100644 legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/maven/TestClasspathUsingMavenFactory.java diff --git a/legend-engine-ide-lsp-default-extensions/pom.xml b/legend-engine-ide-lsp-default-extensions/pom.xml index 32062855..d4d4139d 100644 --- a/legend-engine-ide-lsp-default-extensions/pom.xml +++ b/legend-engine-ide-lsp-default-extensions/pom.xml @@ -595,10 +595,6 @@ org.finos.legend.engine legend-engine-xt-relationalStore-pure - - org.finos.legend.engine - legend-engine-xt-relationalStore-executionPlan-connection-api - org.finos.legend.engine legend-engine-executionPlan-generation diff --git a/legend-engine-ide-lsp-default-extensions/src/main/java/org/finos/legend/engine/ide/lsp/extension/ConnectionLSPGrammarExtension.java b/legend-engine-ide-lsp-default-extensions/src/main/java/org/finos/legend/engine/ide/lsp/extension/ConnectionLSPGrammarExtension.java index 6dcf7289..b5f6a963 100644 --- a/legend-engine-ide-lsp-default-extensions/src/main/java/org/finos/legend/engine/ide/lsp/extension/ConnectionLSPGrammarExtension.java +++ b/legend-engine-ide-lsp-default-extensions/src/main/java/org/finos/legend/engine/ide/lsp/extension/ConnectionLSPGrammarExtension.java @@ -14,8 +14,8 @@ package org.finos.legend.engine.ide.lsp.extension; -import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Collections; +import java.util.List; import java.util.Map; import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.factory.Sets; @@ -27,12 +27,11 @@ import org.finos.legend.engine.language.pure.grammar.from.connection.ConnectionParser; import org.finos.legend.engine.language.pure.grammar.from.extension.PureGrammarParserExtensionLoader; import org.finos.legend.engine.language.pure.grammar.from.extension.PureGrammarParserExtensions; -import org.finos.legend.engine.plan.execution.stores.relational.connection.api.schema.model.DatabaseBuilderInput; -import org.finos.legend.engine.plan.execution.stores.relational.connection.api.schema.model.DatabasePattern; import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.PackageableElement; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.PackageableConnection; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.relational.connection.RelationalDatabaseConnection; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.relational.model.Database; /** * Extension for the Connection grammar. @@ -74,8 +73,8 @@ protected void collectCommands(SectionState sectionState, PackageableElement ele public Iterable execute(SectionState section, String entityPath, String commandId, Map executableArgs) { return GENERATE_DB_COMMAND_ID.equals(commandId) ? - generateDBFromConnection(section, entityPath) : - super.execute(section, entityPath, commandId, executableArgs); + generateDBFromConnection(section, entityPath) : + super.execute(section, entityPath, commandId, executableArgs); } private Iterable generateDBFromConnection(SectionState section, String entityPath) @@ -111,7 +110,7 @@ private DatabaseBuilderInput buildInput(PackageableConnection packageableConn) builderInput.config.enrichTables = true; builderInput.config.enrichColumns = true; builderInput.config.enrichPrimaryKeys = true; - builderInput.config.patterns = Lists.mutable.of(new DatabasePatternFixedTypo("%", "%", "%", false, false)); + builderInput.config.patterns = Lists.mutable.of(new DatabasePattern()); builderInput.targetDatabase.name = packageableConn.name + "Database"; builderInput.targetDatabase._package = packageableConn._package; @@ -125,19 +124,38 @@ private static ListIterable findKeywords() return Lists.immutable.withAll(keywords); } - // todo remove once this is released - https://github.com/finos/legend-engine/pull/2507 - private class DatabasePatternFixedTypo extends DatabasePattern + static class DatabaseBuilderInput { - public DatabasePatternFixedTypo(String catalog, String schemaPattern, String tablePattern, boolean escapeSchemaPattern, boolean escapeTablePattern) - { - super(catalog, schemaPattern, tablePattern, escapeSchemaPattern, escapeTablePattern); - } + public DatabaseBuilderConfig config; + + public RelationalDatabaseConnection connection; + + public Database targetDatabase; - @Override - @JsonProperty("escapteTablePattern") - public boolean isEscapeTablePattern() + public DatabaseBuilderInput() { - return super.isEscapeTablePattern(); + this.config = new DatabaseBuilderConfig(); + this.targetDatabase = new Database(); } } + + static class DatabasePattern + { + public final String catalog = "%"; + + public final String schemaPattern = "%"; + + public final String tablePattern = "%"; + } + + static class DatabaseBuilderConfig + { + public boolean enrichTables; + + public boolean enrichPrimaryKeys; + + public boolean enrichColumns; + + public List patterns = Lists.mutable.empty(); + } } diff --git a/legend-engine-ide-lsp-default-extensions/src/test/java/org/finos/legend/engine/ide/lsp/extension/TestConnectionLSPGrammarExtension.java b/legend-engine-ide-lsp-default-extensions/src/test/java/org/finos/legend/engine/ide/lsp/extension/TestConnectionLSPGrammarExtension.java index 8fc55f8e..8c8af619 100644 --- a/legend-engine-ide-lsp-default-extensions/src/test/java/org/finos/legend/engine/ide/lsp/extension/TestConnectionLSPGrammarExtension.java +++ b/legend-engine-ide-lsp-default-extensions/src/test/java/org/finos/legend/engine/ide/lsp/extension/TestConnectionLSPGrammarExtension.java @@ -27,7 +27,6 @@ import org.finos.legend.engine.ide.lsp.extension.diagnostic.LegendDiagnostic; import org.finos.legend.engine.ide.lsp.extension.execution.LegendExecutionResult; import org.finos.legend.engine.ide.lsp.extension.text.TextInterval; -import org.finos.legend.engine.plan.execution.stores.relational.connection.api.schema.model.DatabaseBuilderInput; import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; import org.finos.legend.engine.shared.core.ObjectMapperFactory; import org.junit.jupiter.api.Assertions; @@ -94,7 +93,7 @@ void testGenerateDBFromConnectionCommand() throws IOException ObjectMapper objectMapper = ObjectMapperFactory.getNewStandardObjectMapperWithPureProtocolExtensionSupports(); try { - DatabaseBuilderInput body = objectMapper.readValue(exchange.getRequestBody(), DatabaseBuilderInput.class); + ConnectionLSPGrammarExtension.DatabaseBuilderInput body = objectMapper.readValue(exchange.getRequestBody(), ConnectionLSPGrammarExtension.DatabaseBuilderInput.class); Assertions.assertEquals("model::MyStore", body.connection.element); Assertions.assertEquals("model", body.targetDatabase._package); Assertions.assertEquals("MyConnectionDatabase", body.targetDatabase.name); diff --git a/legend-engine-ide-lsp-server-shaded/pom.xml b/legend-engine-ide-lsp-server-shaded/pom.xml index 4c07cbbf..340e66f6 100644 --- a/legend-engine-ide-lsp-server-shaded/pom.xml +++ b/legend-engine-ide-lsp-server-shaded/pom.xml @@ -40,10 +40,14 @@ ch.qos.logback:*:${logback.version}:jar:runtime - com.google.code.gson:gson:*:jar:runtime org.eclipse.lsp4j:*:*:jar:runtime org.finos.legend.engine.ide.lsp:*:${project.version}:jar:runtime org.slf4j:slf4j-api:${slf4j.version}:jar:runtime + + com.google.code.gson:gson:*:jar:runtime + org.apache.maven.shared:maven-invoker:${maven.invoker.version}:jar:runtime + org.apache.maven.shared:maven-shared-utils:${maven.shared.utils.version}:jar:runtime + commons-io:commons-io:${commons-io.version}:jar:runtime @@ -78,6 +82,20 @@ org.finos.legend.engine.ide.lsp.server.LegendLanguageServer + + + org.apache.maven.shared + org.finos.legend.engine.ide.lsp.shaded.org.apache.maven.shared + + + org.apache.commons.io + org.finos.legend.engine.ide.lsp.shaded.org.apache.commons.io + + + com.google.gson + org.finos.legend.engine.ide.lsp.shaded.com.google.gson + + diff --git a/legend-engine-ide-lsp-server/pom.xml b/legend-engine-ide-lsp-server/pom.xml index 1783a02a..da5fc2d5 100644 --- a/legend-engine-ide-lsp-server/pom.xml +++ b/legend-engine-ide-lsp-server/pom.xml @@ -44,6 +44,9 @@ org.eclipse.lsp4j:*:${lsp4j.version}:jar:compile org.finos.legend.engine.ide.lsp:*:${project.version}:jar:compile org.slf4j:slf4j-api:${slf4j.version}:jar:compile + org.apache.maven.shared:maven-invoker:${maven.invoker.version}:jar:compile + org.apache.maven.shared:maven-shared-utils:${maven.shared.utils.version}:jar:compile + commons-io:commons-io:${commons-io.version}:jar:compile @@ -64,6 +67,16 @@ + + org.apache.maven.shared + maven-invoker + + + + org.apache.maven.shared + maven-shared-utils + + com.google.code.gson gson diff --git a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/ClasspathFactory.java b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/ClasspathFactory.java new file mode 100644 index 00000000..e90b95d2 --- /dev/null +++ b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/ClasspathFactory.java @@ -0,0 +1,23 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.finos.legend.engine.ide.lsp.classpath; + +import java.util.concurrent.CompletableFuture; +import org.finos.legend.engine.ide.lsp.server.LegendLanguageServer; + +public interface ClasspathFactory +{ + CompletableFuture create(LegendLanguageServer server, Iterable folders); +} diff --git a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/ClasspathUsingMavenFactory.java b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/ClasspathUsingMavenFactory.java new file mode 100644 index 00000000..5ad8b7cf --- /dev/null +++ b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/ClasspathUsingMavenFactory.java @@ -0,0 +1,187 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.finos.legend.engine.ide.lsp.classpath; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.apache.maven.shared.invoker.DefaultInvocationRequest; +import org.apache.maven.shared.invoker.DefaultInvoker; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.Invoker; +import org.apache.maven.shared.invoker.InvokerLogger; +import org.apache.maven.shared.invoker.PrintStreamHandler; +import org.apache.maven.shared.invoker.PrintStreamLogger; +import org.apache.maven.shared.utils.Os; +import org.apache.maven.shared.utils.cli.CommandLineUtils; +import org.apache.maven.shared.utils.cli.Commandline; +import org.eclipse.lsp4j.ConfigurationItem; +import org.eclipse.lsp4j.ConfigurationParams; +import org.finos.legend.engine.ide.lsp.server.LegendLanguageServer; + +public class ClasspathUsingMavenFactory implements ClasspathFactory +{ + private final Invoker invoker; + private final File defaultPom; + private final ByteArrayOutputStream outputStream; + + public ClasspathUsingMavenFactory(File defaultPom) + { + this.defaultPom = defaultPom; + this.invoker = new DefaultInvoker(); + this.outputStream = new ByteArrayOutputStream(); + this.invoker.setLogger(new PrintStreamLogger(new PrintStream(this.outputStream, true), InvokerLogger.INFO)); + } + + private static File getMavenExecLocation(String mavenHome) throws Exception + { + if (mavenHome == null || mavenHome.isEmpty()) + { + Commandline commandline = new Commandline(); + + if (Os.isFamily(Os.FAMILY_WINDOWS)) + { + commandline.setExecutable("where"); + commandline.addArguments("mvn"); + } + else if (Os.isFamily(Os.FAMILY_UNIX)) + { + commandline.setExecutable("which"); + commandline.addArguments("mvn"); + } + else + { + throw new UnsupportedOperationException("OS not supported"); + } + + CommandLineUtils.StringStreamConsumer systemOut = new CommandLineUtils.StringStreamConsumer(); + CommandLineUtils.StringStreamConsumer systemErr = new CommandLineUtils.StringStreamConsumer(); + int result = CommandLineUtils.executeCommandLine(commandline, systemOut, systemErr, 2); + + if (result == 0) + { + String[] split = systemOut.getOutput().split(System.lineSeparator()); + if (split.length == 0) + { + return null; + } + String location = split[0]; + return new File(location); + } + else + { + throw new RuntimeException("Error finding mvn executable: " + systemErr.getOutput()); + } + } + else + { + return new File(mavenHome); + } + } + + @Override + public CompletableFuture create(LegendLanguageServer server, Iterable folders) + { + server.logInfoToClient("Discovering classpath using maven"); + + ConfigurationItem mavenExecPathConfig = new ConfigurationItem(); + mavenExecPathConfig.setSection("maven.executable.path"); + + ConfigurationItem defaultPomConfig = new ConfigurationItem(); + defaultPomConfig.setSection("legend.extensions.dependencies.pom"); + + ConfigurationParams configurationParams = new ConfigurationParams(Arrays.asList(mavenExecPathConfig, defaultPomConfig)); + return server.getLanguageClient().configuration(configurationParams).thenApply(x -> + { + String mavenExecPath = server.extractValueAs(x.get(0), String.class); + String overrideDefaultPom = server.extractValueAs(x.get(1), String.class); + + try + { + File maven = getMavenExecLocation(mavenExecPath); + server.logInfoToClient("Maven path: " + maven); + + File pom = (overrideDefaultPom == null || overrideDefaultPom.isEmpty()) ? this.defaultPom : new File(overrideDefaultPom); + + // todo apply properties from /project.json is this exists... + // todo if project.json exists, use pom from a sub-module + // todo otherwise, check if pom exists on root + // todo last, use a default pom... + + server.logInfoToClient("Dependencies loaded from POM: " + pom); + + File legendLspClasspath = File.createTempFile("legend_lsp_classpath", ".txt"); + legendLspClasspath.deleteOnExit(); + + Properties properties = new Properties(); + properties.setProperty("mdep.outputFile", legendLspClasspath.getAbsolutePath()); + + InvocationRequest request = new DefaultInvocationRequest(); + request.setPomFile(pom); + request.setOutputHandler(new PrintStreamHandler(new PrintStream(this.outputStream, true), true)); + request.setGoals(Collections.singletonList("dependency:build-classpath")); + request.setProperties(properties); + request.setTimeoutInSeconds((int) TimeUnit.MINUTES.toSeconds(5)); + request.setJavaHome(Optional.ofNullable(System.getProperty("java.home")).map(File::new).orElse(null)); + request.setMavenHome(maven); + + InvocationResult result = this.invoker.execute(request); + if (result.getExitCode() != 0) + { + String output = this.outputStream.toString(StandardCharsets.UTF_8); + throw new IllegalStateException("Maven invoker failed\n\n" + output, result.getExecutionException()); + } + + String classpath = Files.readString(legendLspClasspath.toPath(), StandardCharsets.UTF_8); + + server.logInfoToClient("Classpath used: " + classpath); + + String[] classpathEntries = classpath.split(";"); + URL[] urls = new URL[classpathEntries.length]; + + for (int i = 0; i < urls.length; i++) + { + urls[i] = new File(classpathEntries[i]).toURI().toURL(); + } + + ClassLoader parentClassloader = ClasspathUsingMavenFactory.class.getClassLoader(); + return new URLClassLoader("legend-lsp", urls, parentClassloader); + } + catch (RuntimeException e) + { + throw e; + } + catch (Exception e) + { + throw new RuntimeException(e); + } + finally + { + this.outputStream.reset(); + } + }); + } +} diff --git a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/EmbeddedClasspathFactory.java b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/EmbeddedClasspathFactory.java new file mode 100644 index 00000000..6590cfed --- /dev/null +++ b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/classpath/EmbeddedClasspathFactory.java @@ -0,0 +1,28 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.finos.legend.engine.ide.lsp.classpath; + +import java.util.concurrent.CompletableFuture; +import org.finos.legend.engine.ide.lsp.server.LegendLanguageServer; + +public class EmbeddedClasspathFactory implements ClasspathFactory +{ + @Override + public CompletableFuture create(LegendLanguageServer server, Iterable folders) + { + server.logInfoToClient("Using app classpath"); + return CompletableFuture.completedFuture(EmbeddedClasspathFactory.class.getClassLoader()); + } +} diff --git a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/ExtensionsGuard.java b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/ExtensionsGuard.java new file mode 100644 index 00000000..b106d195 --- /dev/null +++ b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/ExtensionsGuard.java @@ -0,0 +1,120 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.finos.legend.engine.ide.lsp.server; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.function.Supplier; +import org.finos.legend.engine.ide.lsp.extension.LegendLSPGrammarExtension; +import org.finos.legend.engine.ide.lsp.extension.LegendLSPGrammarLibrary; +import org.finos.legend.engine.ide.lsp.extension.LegendLSPInlineDSLExtension; +import org.finos.legend.engine.ide.lsp.extension.LegendLSPInlineDSLLibrary; + +class ExtensionsGuard +{ + private final Iterable providedGrammarExtensions; + private final Iterable providedInlineDSLs; + private final LegendLanguageServer server; + private volatile ClassLoader classLoader; + private volatile LegendLSPGrammarLibrary grammars; + private volatile LegendLSPInlineDSLLibrary inlineDSLs; + + public ExtensionsGuard(LegendLanguageServer server, LegendLSPGrammarLibrary providedGrammarExtensions, LegendLSPInlineDSLLibrary providedInlineDSLs) + { + this.server = server; + this.grammars = providedGrammarExtensions; + this.inlineDSLs = providedInlineDSLs; + this.providedGrammarExtensions = providedGrammarExtensions.getExtensions(); + this.providedInlineDSLs = providedInlineDSLs.getExtensions(); + } + + public synchronized void initialize(ClassLoader classLoader) + { + this.server.logInfoToClient("Initializing grammar extensions"); + + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + + try + { + if (this.classLoader != null && this.classLoader instanceof Closeable) + { + ((Closeable) this.classLoader).close(); + } + + Thread.currentThread().setContextClassLoader(classLoader); + + this.grammars = LegendLSPGrammarLibrary.builder().withExtensions(this.providedGrammarExtensions).withExtensionsFrom(classLoader).build(); + this.inlineDSLs = LegendLSPInlineDSLLibrary.builder().withExtensions(this.providedInlineDSLs).withExtensionsFrom(classLoader).build(); + + this.server.logInfoToClient("Grammar extensions available: " + String.join(", ", this.grammars.getExtensionNames())); + this.server.logInfoToClient("Inline DSL extensions available: " + String.join(", ", this.inlineDSLs.getExtensionNames())); + + this.classLoader = classLoader; + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + finally + { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + + public LegendLSPGrammarLibrary getGrammars() + { + return this.grammars; + } + + public LegendLSPInlineDSLLibrary getInlineDSLs() + { + return this.inlineDSLs; + } + + Runnable wrapOnClasspath(Runnable command) + { + return () -> + { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try + { + Thread.currentThread().setContextClassLoader(this.classLoader); + command.run(); + } + finally + { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + }; + } + + Supplier wrapOnClasspath(Supplier command) + { + return () -> + { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try + { + Thread.currentThread().setContextClassLoader(this.classLoader); + return command.get(); + } + finally + { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + }; + } +} diff --git a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendLanguageServer.java b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendLanguageServer.java index ceb14bc6..acddb13e 100644 --- a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendLanguageServer.java +++ b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendLanguageServer.java @@ -17,7 +17,25 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Collectors; import org.eclipse.lsp4j.CodeLensOptions; +import org.eclipse.lsp4j.ConfigurationItem; +import org.eclipse.lsp4j.ConfigurationParams; import org.eclipse.lsp4j.ExecuteCommandOptions; import org.eclipse.lsp4j.FileOperationFilter; import org.eclipse.lsp4j.FileOperationOptions; @@ -53,6 +71,9 @@ import org.eclipse.lsp4j.services.LanguageServer; import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; +import org.finos.legend.engine.ide.lsp.classpath.ClasspathFactory; +import org.finos.legend.engine.ide.lsp.classpath.ClasspathUsingMavenFactory; +import org.finos.legend.engine.ide.lsp.classpath.EmbeddedClasspathFactory; import org.finos.legend.engine.ide.lsp.extension.LegendLSPGrammarExtension; import org.finos.legend.engine.ide.lsp.extension.LegendLSPGrammarLibrary; import org.finos.legend.engine.ide.lsp.extension.LegendLSPInlineDSLExtension; @@ -61,21 +82,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import java.util.stream.Collectors; - /** * {@link LanguageServer} implementation for Legend. */ @@ -95,44 +101,148 @@ public class LegendLanguageServer implements LanguageServer, LanguageClientAware private final LegendWorkspaceService workspaceService; private final AtomicReference languageClient = new AtomicReference<>(null); private final AtomicInteger state = new AtomicInteger(UNINITIALIZED); - private final boolean async; + private final ClasspathFactory classpathFactory; + private final ExtensionsGuard extensionGuard; private final Executor executor; - private final LegendLSPGrammarLibrary grammars; - private final LegendLSPInlineDSLLibrary inlineDSLs; + private final boolean async; private final LegendServerGlobalState globalState = new LegendServerGlobalState(this); private final AtomicInteger progressId = new AtomicInteger(); private final Gson gson = new Gson(); - private final Set rootFolders = new HashSet<>(); - private LegendLanguageServer(boolean async, Executor executor, LegendLSPGrammarLibrary grammars, LegendLSPInlineDSLLibrary inlineDSLs) + private LegendLanguageServer(boolean async, Executor executor, ClasspathFactory classpathFactory, LegendLSPGrammarLibrary grammars, LegendLSPInlineDSLLibrary inlineDSLs) { this.textDocumentService = new LegendTextDocumentService(this); this.workspaceService = new LegendWorkspaceService(this); + this.extensionGuard = new ExtensionsGuard(this, grammars, inlineDSLs); this.async = async; this.executor = executor; - this.grammars = grammars; - this.inlineDSLs = inlineDSLs; + this.classpathFactory = Objects.requireNonNull(classpathFactory, "missing classpath factory"); } @Override public CompletableFuture initialize(InitializeParams initializeParams) { LOGGER.debug("Initialize server requested: {}", initializeParams); - int currentState = this.state.get(); - if (currentState >= INITIALIZING) + if (this.state.get() >= INITIALIZING) { - String message = getCannotInitializeMessage(currentState); + String message = getCannotInitializeMessage(this.state.get()); LOGGER.error(message); throw newResponseErrorException(ResponseErrorCode.RequestFailed, message); } - return supplyPossiblyAsync_internal(() -> doInitialize(initializeParams)); + + LOGGER.info("Initializing server"); + LOGGER.debug("Initialize params: {}", initializeParams); + if (!this.state.compareAndSet(UNINITIALIZED, INITIALIZING)) + { + String message = getCannotInitializeMessage(this.state.get()); + LOGGER.warn(message); + logWarningToClient(message); + throw newResponseErrorException(ResponseErrorCode.RequestFailed, message); + } + + logInfoToClient("Initializing server"); + List workspaceFolders = initializeParams.getWorkspaceFolders(); + + InitializeResult result = new InitializeResult(getServerCapabilities()); + CompletableFuture completableFuture = CompletableFuture.completedFuture(result); + CompletableFuture initFuture = completableFuture; + + if (workspaceFolders != null) + { + initFuture = setWorkspaceFolders(workspaceFolders).thenComposeAsync(x -> completableFuture); + } + + if (!this.state.compareAndSet(INITIALIZING, INITIALIZED)) + { + String message; + switch (this.state.get()) + { + case SHUTTING_DOWN: + { + message = "Server began shutting down during initialization"; + break; + } + case SHUT_DOWN: + { + message = "Server shut down during initialization"; + break; + } + default: + { + message = "Server entered unexpected state during initialization: " + getStateDescription(this.state.get()); + } + } + LOGGER.warn(message); + logWarningToClient(message); + throw newResponseErrorException(ResponseErrorCode.RequestFailed, message); + } + LOGGER.info("Server initialized"); + logInfoToClient("Server initialized"); + return initFuture; } @Override public void initialized(InitializedParams params) { checkReady(); + this.initializeExtensions(); + this.initializeEngineServerUrl(); + } + + private void initializeEngineServerUrl() + { + ConfigurationItem urlConfig = new ConfigurationItem(); + urlConfig.setSection("legend.engine.server.url"); + + ConfigurationParams configurationParams = new ConfigurationParams(Arrays.asList(urlConfig)); + this.getLanguageClient().configuration(configurationParams).thenAccept(x -> + { + String url = this.extractValueAs(x.get(0), String.class); + this.setEngineServerUrl(url); + }); + } + + private void setEngineServerUrl(String url) + { + if (url != null && !url.isEmpty()) + { + this.logInfoToClient("Using server URL: " + url); + System.setProperty("legend.engine.server.url", url); + } + else + { + this.logWarningToClient("No server URL found. Some functionality won't work"); + System.clearProperty("legend.engine.server.url"); + } + } + + private void initializeExtensions() + { + logInfoToClient("Initializing extensions"); + + this.classpathFactory.create(this, Collections.unmodifiableSet(this.rootFolders)) + .thenAccept(this.extensionGuard::initialize) + .thenRun(this.extensionGuard.wrapOnClasspath(this::reprocessDocuments)) + .thenRun(() -> + { + LanguageClient languageClient = this.getLanguageClient(); + languageClient.refreshCodeLenses(); + languageClient.refreshDiagnostics(); + languageClient.refreshInlayHints(); + languageClient.refreshInlineValues(); + languageClient.refreshSemanticTokens(); + }).exceptionally(x -> + { + LOGGER.error("Failed during post-initialization", x); + logErrorToClient("Failed during post-initialization: " + x.getMessage()); + return null; + }); + } + + private void reprocessDocuments() + { + this.globalState.forEachDocumentState(x -> ((LegendServerGlobalState.LegendServerDocumentState) x).recreateSectionStates()); } @Override @@ -145,7 +255,7 @@ public CompletableFuture shutdown() LOGGER.warn("Server already {}", getStateDescription(currentState)); return CompletableFuture.completedFuture(null); } - return supplyPossiblyAsync_internal(() -> + return this.supplyPossiblyAsync_internal(() -> { doShutdown(); return null; @@ -290,13 +400,35 @@ void checkNotShutDown() CompletableFuture supplyPossiblyAsync(Supplier supplier) { checkReady(); - return supplyPossiblyAsync_internal(supplier); + return this.supplyPossiblyAsync_internal(this.extensionGuard.wrapOnClasspath(supplier)); } CompletableFuture runPossiblyAsync(Runnable runnable) { checkReady(); - return runPossiblyAsync_internal(runnable); + return this.runPossiblyAsync_internal(this.extensionGuard.wrapOnClasspath(runnable)); + } + + private CompletableFuture runPossiblyAsync_internal(Runnable work) + { + return this.supplyPossiblyAsync_internal(() -> + { + work.run(); + return null; + } + ); + } + + private CompletableFuture supplyPossiblyAsync_internal(Supplier work) + { + if (this.async) + { + return (this.executor == null) ? + CompletableFuture.supplyAsync(work) : + CompletableFuture.supplyAsync(work, this.executor); + } + + return CompletableFuture.completedFuture(work.get()); } LegendServerGlobalState getGlobalState() @@ -305,7 +437,7 @@ LegendServerGlobalState getGlobalState() return this.globalState; } - LanguageClient getLanguageClient() + public LanguageClient getLanguageClient() { checkNotShutDown(); return this.languageClient.get(); @@ -314,22 +446,22 @@ LanguageClient getLanguageClient() LegendLSPGrammarLibrary getGrammarLibrary() { checkNotShutDown(); - return this.grammars; + return this.extensionGuard.getGrammars(); } LegendLSPGrammarExtension getGrammarExtension(String grammar) { checkNotShutDown(); - return this.grammars.getExtension(grammar); + return this.extensionGuard.getGrammars().getExtension(grammar); } LegendLSPInlineDSLLibrary getInlineDSLLibrary() { checkNotShutDown(); - return this.inlineDSLs; + return this.extensionGuard.getInlineDSLs(); } - void setWorkspaceFolders(Iterable folders) + CompletableFuture setWorkspaceFolders(Iterable folders) { List addedFolders; List removedFolders; @@ -346,7 +478,7 @@ void setWorkspaceFolders(Iterable folders) this.rootFolders.addAll(newRootFolders); } - updateStateWithFolderChanges(addedFolders, removedFolders); + return updateStateWithFolderChanges(addedFolders, removedFolders); } void addRootFolderFromFile(String fileUri) @@ -373,7 +505,7 @@ void addRootFolder(String folderUri) String message = "Added root folder: " + folderUri; LOGGER.info(message); logInfoToClient(message); - runPossiblyAsync_internal(() -> this.globalState.addFolder(folderUri)); + this.runPossiblyAsync_internal(() -> this.globalState.addFolder(folderUri)); } } @@ -410,7 +542,7 @@ void updateWorkspaceFolders(Iterable foldersToAdd, It } } - private void updateStateWithFolderChanges(List addedFolders, List removedFolders) + private CompletableFuture updateStateWithFolderChanges(Collection addedFolders, Collection removedFolders) { if ((addedFolders != null) && !addedFolders.isEmpty()) { @@ -424,17 +556,17 @@ private void updateStateWithFolderChanges(List addedFolders, List + return this.runPossiblyAsync_internal(this.extensionGuard.wrapOnClasspath(() -> { - if (addedFolders != null) - { - addedFolders.forEach(this.globalState::addFolder); - } - if (removedFolders != null) - { - removedFolders.forEach(this.globalState::removeFolder); - } - }); + if (addedFolders != null) + { + addedFolders.forEach(this.globalState::addFolder); + } + if (removedFolders != null) + { + removedFolders.forEach(this.globalState::removeFolder); + } + })); } void logToClient(String message) @@ -442,7 +574,7 @@ void logToClient(String message) logToClient(MessageType.Log, message); } - void logInfoToClient(String message) + public void logInfoToClient(String message) { logToClient(MessageType.Info, message); } @@ -573,7 +705,7 @@ private void notifyProgress(Either token, WorkDoneProgressNotif } @SuppressWarnings("unchecked") - T extractValueAs(Object value, Class cls) + public T extractValueAs(Object value, Class cls) { if (value == null) { @@ -616,87 +748,6 @@ Map extractValueAsMap(Object value, Class keyType, Class valu return null; } - private CompletableFuture supplyPossiblyAsync_internal(Supplier supplier) - { - return this.async ? - supplyAsync(supplier) : - CompletableFuture.completedFuture(supplier.get()); - } - - private CompletableFuture runPossiblyAsync_internal(Runnable runnable) - { - if (this.async) - { - return runAsync(runnable); - } - - runnable.run(); - return CompletableFuture.completedFuture(null); - } - - private CompletableFuture supplyAsync(Supplier supplier) - { - return (this.executor == null) ? - CompletableFuture.supplyAsync(supplier) : - CompletableFuture.supplyAsync(supplier, this.executor); - } - - private CompletableFuture runAsync(Runnable runnable) - { - return (this.executor == null) ? - CompletableFuture.runAsync(runnable) : - CompletableFuture.runAsync(runnable, this.executor); - } - - private InitializeResult doInitialize(InitializeParams initializeParams) - { - LOGGER.info("Initializing server"); - LOGGER.debug("Initialize params: {}", initializeParams); - if (!this.state.compareAndSet(UNINITIALIZED, INITIALIZING)) - { - String message = getCannotInitializeMessage(this.state.get()); - LOGGER.warn(message); - logWarningToClient(message); - throw newResponseErrorException(ResponseErrorCode.RequestFailed, message); - } - - logInfoToClient("Initializing server"); - List workspaceFolders = initializeParams.getWorkspaceFolders(); - if (workspaceFolders != null) - { - setWorkspaceFolders(workspaceFolders); - } - InitializeResult result = new InitializeResult(getServerCapabilities()); - if (!this.state.compareAndSet(INITIALIZING, INITIALIZED)) - { - int currentState = this.state.get(); - String message; - switch (currentState) - { - case SHUTTING_DOWN: - { - message = "Server began shutting down during initialization"; - break; - } - case SHUT_DOWN: - { - message = "Server shut down during initialization"; - break; - } - default: - { - message = "Server entered unexpected state during initialization: " + getStateDescription(currentState); - } - } - LOGGER.warn(message); - logWarningToClient(message); - throw newResponseErrorException(ResponseErrorCode.RequestFailed, message); - } - LOGGER.info("Server initialized"); - logInfoToClient("Server initialized"); - return result; - } - private String getCannotInitializeMessage(int currentState) { switch (currentState) @@ -872,6 +923,7 @@ public static class Builder { private boolean async = true; private Executor executor; + private ClasspathFactory classpathFactory = new EmbeddedClasspathFactory(); private final LegendLSPGrammarLibrary.Builder grammars = LegendLSPGrammarLibrary.builder(); private final LegendLSPInlineDSLLibrary.Builder inlineDSLs = LegendLSPInlineDSLLibrary.builder(); @@ -891,6 +943,12 @@ public Builder synchronous() return this; } + public Builder classpathFromMaven(File defaultPom) + { + this.classpathFactory = new ClasspathUsingMavenFactory(defaultPom); + return this; + } + /** * Set the server to perform operations asynchronously. * @@ -1000,7 +1058,7 @@ public Builder withInlineDSLs(Iterable in */ public LegendLanguageServer build() { - return new LegendLanguageServer(this.async, this.executor, this.grammars.build(), this.inlineDSLs.build()); + return new LegendLanguageServer(this.async, this.executor, this.classpathFactory, this.grammars.build(), this.inlineDSLs.build()); } } @@ -1014,8 +1072,7 @@ public static void main(String[] args) { LOGGER.info("Launching server"); LegendLanguageServer server = LegendLanguageServer.builder() - .withGrammars(ServiceLoader.load(LegendLSPGrammarExtension.class)) - .withInlineDSLs(ServiceLoader.load(LegendLSPInlineDSLExtension.class)) + .classpathFromMaven(new File(args[0])) .build(); Launcher launcher = LSPLauncher.createServerLauncher(server, System.in, System.out); server.connect(launcher.getRemoteProxy()); diff --git a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendServerGlobalState.java b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendServerGlobalState.java index ab9f8038..0ced1012 100644 --- a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendServerGlobalState.java +++ b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendServerGlobalState.java @@ -397,6 +397,15 @@ private void setText(String newText) this.sectionStates = createSectionStates(this.sectionIndex); } + public void recreateSectionStates() + { + synchronized (this.lock) + { + this.globalState.clearProperties(); + this.sectionStates = createSectionStates(this.sectionIndex); + } + } + private List createSectionStates(GrammarSectionIndex index) { if (index == null) diff --git a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendTextDocumentService.java b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendTextDocumentService.java index 75b623dd..23114b19 100644 --- a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendTextDocumentService.java +++ b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendTextDocumentService.java @@ -14,6 +14,14 @@ package org.finos.legend.engine.ide.lsp.server; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.CodeLensParams; import org.eclipse.lsp4j.Command; @@ -52,15 +60,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - class LegendTextDocumentService implements TextDocumentService { private static final Logger LOGGER = LoggerFactory.getLogger(LegendTextDocumentService.class); @@ -75,103 +74,115 @@ class LegendTextDocumentService implements TextDocumentService @Override public void didOpen(DidOpenTextDocumentParams params) { - LegendServerGlobalState globalState = this.server.getGlobalState(); - synchronized (globalState) + this.server.runPossiblyAsync(() -> { - this.server.checkReady(); - TextDocumentItem doc = params.getTextDocument(); - String uri = doc.getUri(); - LOGGER.debug("Opened {} (language id: {}, version: {})", uri, doc.getLanguageId(), doc.getVersion()); - LegendServerDocumentState docState = globalState.getOrCreateDocState(uri); - docState.open(doc.getVersion(), doc.getText()); - } + LegendServerGlobalState globalState = this.server.getGlobalState(); + synchronized (globalState) + { + this.server.checkReady(); + TextDocumentItem doc = params.getTextDocument(); + String uri = doc.getUri(); + LOGGER.debug("Opened {} (language id: {}, version: {})", uri, doc.getLanguageId(), doc.getVersion()); + LegendServerDocumentState docState = globalState.getOrCreateDocState(uri); + docState.open(doc.getVersion(), doc.getText()); + } + }); } @Override public void didChange(DidChangeTextDocumentParams params) { - LegendServerGlobalState globalState = this.server.getGlobalState(); - synchronized (globalState) + this.server.runPossiblyAsync(() -> { - this.server.checkReady(); - VersionedTextDocumentIdentifier doc = params.getTextDocument(); - String uri = doc.getUri(); - LOGGER.debug("Changed {} (version {})", uri, doc.getVersion()); - - LegendServerDocumentState docState = globalState.getDocumentState(uri); - if (docState == null) + LegendServerGlobalState globalState = this.server.getGlobalState(); + synchronized (globalState) { - LOGGER.warn("Change to {} (version {}) before it was opened", uri, doc.getVersion()); - docState = globalState.getOrCreateDocState(uri); - } + this.server.checkReady(); + VersionedTextDocumentIdentifier doc = params.getTextDocument(); + String uri = doc.getUri(); + LOGGER.debug("Changed {} (version {})", uri, doc.getVersion()); - List changes = params.getContentChanges(); - if ((changes == null) || changes.isEmpty()) - { - LOGGER.debug("No changes to {}", uri); - docState.change(doc.getVersion()); - return; - } + LegendServerDocumentState docState = globalState.getDocumentState(uri); + if (docState == null) + { + LOGGER.warn("Change to {} (version {}) before it was opened", uri, doc.getVersion()); + docState = globalState.getOrCreateDocState(uri); + } - if (changes.size() > 1) - { - String message = "Expected at most one change to " + uri + ", got " + changes.size() + "; processing only the first"; - LOGGER.warn(message); - this.server.logWarningToClient(message); - } - docState.change(doc.getVersion(), changes.get(0).getText()); - try - { - publishDiagnosticsToClient(docState); - } - catch (Exception e) - { - LOGGER.error("Error publishing diagnostics for {} to client", uri, e); - this.server.logErrorToClient("Error publishing diagnostics for " + uri + " to client: see server log for more details"); + List changes = params.getContentChanges(); + if ((changes == null) || changes.isEmpty()) + { + LOGGER.debug("No changes to {}", uri); + docState.change(doc.getVersion()); + return; + } + + if (changes.size() > 1) + { + String message = "Expected at most one change to " + uri + ", got " + changes.size() + "; processing only the first"; + LOGGER.warn(message); + this.server.logWarningToClient(message); + } + docState.change(doc.getVersion(), changes.get(0).getText()); + try + { + publishDiagnosticsToClient(docState); + } + catch (Exception e) + { + LOGGER.error("Error publishing diagnostics for {} to client", uri, e); + this.server.logErrorToClient("Error publishing diagnostics for " + uri + " to client: see server log for more details"); + } } - } + }); } @Override public void didClose(DidCloseTextDocumentParams params) { - LegendServerGlobalState globalState = this.server.getGlobalState(); - synchronized (globalState) + this.server.runPossiblyAsync(() -> { - this.server.checkReady(); - String uri = params.getTextDocument().getUri(); - LOGGER.debug("Closed {}", uri); - LegendServerDocumentState docState = globalState.getDocumentState(uri); - if (docState == null) - { - LOGGER.warn("Closed notification for a document that is not open: {}", uri); - } - else + LegendServerGlobalState globalState = this.server.getGlobalState(); + synchronized (globalState) { - docState.close(); + this.server.checkReady(); + String uri = params.getTextDocument().getUri(); + LOGGER.debug("Closed {}", uri); + LegendServerDocumentState docState = globalState.getDocumentState(uri); + if (docState == null) + { + LOGGER.warn("Closed notification for a document that is not open: {}", uri); + } + else + { + docState.close(); + } } - } + }); } @Override public void didSave(DidSaveTextDocumentParams params) { - LegendServerGlobalState globalState = this.server.getGlobalState(); - synchronized (globalState) + this.server.runPossiblyAsync(() -> { - this.server.checkReady(); - String uri = params.getTextDocument().getUri(); - LOGGER.debug("Saved {}", uri); - LegendServerDocumentState docState = globalState.getDocumentState(uri); - if (docState == null) - { - LOGGER.warn("Saved notification for a document that is not open: {}", uri); - } - else + LegendServerGlobalState globalState = this.server.getGlobalState(); + synchronized (globalState) { - docState.save(params.getText()); + this.server.checkReady(); + String uri = params.getTextDocument().getUri(); + LOGGER.debug("Saved {}", uri); + LegendServerDocumentState docState = globalState.getDocumentState(uri); + if (docState == null) + { + LOGGER.warn("Saved notification for a document that is not open: {}", uri); + } + else + { + docState.save(params.getText()); + } } - } + }); } @Override diff --git a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendWorkspaceService.java b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendWorkspaceService.java index 196f0d04..92957c3b 100644 --- a/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendWorkspaceService.java +++ b/legend-engine-ide-lsp-server/src/main/java/org/finos/legend/engine/ide/lsp/server/LegendWorkspaceService.java @@ -167,11 +167,17 @@ public void didChangeConfiguration(DidChangeConfigurationParams params) { this.server.checkReady(); LOGGER.debug("Did change configuration: {}", params); + // todo maybe server can have a map of configs to consumers of these properties + // todo if default pom changes? this.server.initializeExtensions(); + // todo this.server.setEngineServerUrl(); } @Override public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { + // todo - retrigger classpath / classloader init? + // todo this.server.initializeExtensions(); + LOGGER.debug("Did change watched files: {}", params); this.server.runPossiblyAsync(() -> { @@ -211,6 +217,8 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) @Override public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { + // todo - retrigger classpath / classloader init? + LOGGER.debug("Did change workspace folders: {}", params); synchronized (this.server.getGlobalState()) { @@ -222,6 +230,8 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) @Override public void didCreateFiles(CreateFilesParams params) { + // todo - retrigger classpath / classloader init? + LOGGER.debug("Did create files: {}", params); this.server.runPossiblyAsync(() -> { diff --git a/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/DummyLanguageClient.java b/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/DummyLanguageClient.java new file mode 100644 index 00000000..0faf1b2d --- /dev/null +++ b/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/DummyLanguageClient.java @@ -0,0 +1,104 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.finos.legend.engine.ide.lsp; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.eclipse.lsp4j.ConfigurationItem; +import org.eclipse.lsp4j.ConfigurationParams; +import org.eclipse.lsp4j.MessageActionItem; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.services.LanguageClient; + +public class DummyLanguageClient implements LanguageClient +{ + public final List clientLog = new ArrayList<>(); + + @Override + public void telemetryEvent(Object object) + { + clientLog.add("telemetryEvent"); + } + + @Override + public void publishDiagnostics(PublishDiagnosticsParams diagnostics) + { + clientLog.add("publishDiagnostics"); + } + + @Override + public void showMessage(MessageParams messageParams) + { + clientLog.add("showMessage"); + } + + @Override + public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) + { + throw new UnsupportedOperationException(); + } + + @Override + public void logMessage(MessageParams message) + { + clientLog.add(String.format("logMessage - %s - %s", message.getType().name(), message.getMessage())); + } + + @Override + public CompletableFuture> configuration(ConfigurationParams configurationParams) + { + clientLog.add(String.format("configuration - %s", configurationParams.getItems().stream().map(ConfigurationItem::getSection).collect(Collectors.joining()))); + return CompletableFuture.completedFuture(configurationParams.getItems().stream().map(x -> null).collect(Collectors.toList())); + } + + @Override + public CompletableFuture refreshSemanticTokens() + { + clientLog.add("refreshSemanticTokens"); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture refreshCodeLenses() + { + clientLog.add("refreshCodeLenses"); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture refreshInlayHints() + { + clientLog.add("refreshInlayHints"); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture refreshInlineValues() + { + clientLog.add("refreshInlineValues"); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture refreshDiagnostics() + { + clientLog.add("refreshDiagnostics"); + return CompletableFuture.completedFuture(null); + } +} diff --git a/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/maven/TestClasspathUsingMavenFactory.java b/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/maven/TestClasspathUsingMavenFactory.java new file mode 100644 index 00000000..6b2de73e --- /dev/null +++ b/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/maven/TestClasspathUsingMavenFactory.java @@ -0,0 +1,66 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.finos.legend.engine.ide.lsp.maven; + +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import org.finos.legend.engine.ide.lsp.DummyLanguageClient; +import org.finos.legend.engine.ide.lsp.classpath.ClasspathUsingMavenFactory; +import org.finos.legend.engine.ide.lsp.server.LegendLanguageServer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class TestClasspathUsingMavenFactory +{ + @Test + void loadJarsFromPom(@TempDir Path tempDir) throws Exception + { + Path pom = tempDir.resolve("pom.xml"); + + Files.writeString(pom, + "\n" + + " 4.0.0\n" + + "\n" + + " org.finos.legend.engine.ide.lsp\n" + + " legend-engine-ide-lsp-server-sample-pom\n" + + " 0.0.0-SNAPSHOT\n" + + "\n" + + " \n" + + " \n" + + " commons-io\n" + + " commons-io\n" + + " 2.15.1\n" + + " \n" + + " \n" + + "", StandardCharsets.UTF_8); + + LegendLanguageServer server = LegendLanguageServer.builder().synchronous().build(); + DummyLanguageClient languageClient = new DummyLanguageClient(); + server.connect(languageClient); + + ClassLoader classLoader = new ClasspathUsingMavenFactory(pom.toFile()) + .create(server, Collections.emptyList()).get(); + + try (URLClassLoader urlClassLoader = Assertions.assertInstanceOf(URLClassLoader.class, classLoader)) + { + Assertions.assertEquals(1, urlClassLoader.getURLs().length); + Assertions.assertTrue(urlClassLoader.getURLs()[0].toString().endsWith("commons-io-2.15.1.jar")); + } + } +} diff --git a/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/server/TestLegendLanguageServer.java b/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/server/TestLegendLanguageServer.java index e6a07b9b..27bdb478 100644 --- a/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/server/TestLegendLanguageServer.java +++ b/legend-engine-ide-lsp-server/src/test/java/org/finos/legend/engine/ide/lsp/server/TestLegendLanguageServer.java @@ -14,28 +14,23 @@ package org.finos.legend.engine.ide.lsp.server; +import java.util.Set; import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.InitializedParams; -import org.eclipse.lsp4j.MessageActionItem; -import org.eclipse.lsp4j.MessageParams; -import org.eclipse.lsp4j.PublishDiagnosticsParams; -import org.eclipse.lsp4j.ShowMessageRequestParams; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.services.LanguageClient; +import org.finos.legend.engine.ide.lsp.DummyLanguageClient; import org.finos.legend.engine.ide.lsp.extension.LegendLSPGrammarExtension; import org.finos.legend.engine.ide.lsp.extension.LegendLSPInlineDSLExtension; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; -import java.util.Set; -import java.util.concurrent.CompletableFuture; - public class TestLegendLanguageServer { @Test @@ -43,6 +38,7 @@ public void testServerInitShutdown() throws Exception { // Initial state LegendLanguageServer server = LegendLanguageServer.builder().synchronous().build(); + server.connect(newLanguageClient()); Assertions.assertTrue(server.isUninitialized()); Assertions.assertFalse(server.isInitialized()); Assertions.assertFalse(server.isShutDown()); @@ -64,7 +60,7 @@ public void testServerInitShutdown() throws Exception server.initialized(new InitializedParams()); Assertions.assertInstanceOf(LegendWorkspaceService.class, server.getWorkspaceService()); Assertions.assertInstanceOf(LegendTextDocumentService.class, server.getTextDocumentService()); - Assertions.assertNull(server.getLanguageClient()); + Assertions.assertNotNull(server.getLanguageClient()); // Shut down Object shutDownResult = server.shutdown().get(); @@ -154,6 +150,23 @@ public void testInlineDSLs() Assertions.assertEquals("Multiple extensions named: \"ext1\"", e.getMessage()); } + @Test + void testInitializedReloadExtensions() throws Exception + { + LegendLanguageServer server = LegendLanguageServer.builder().synchronous().build(); + DummyLanguageClient languageClient = newLanguageClient(); + server.connect(languageClient); + server.initialize(new InitializeParams()).get(); + Assertions.assertTrue(server.getGrammarLibrary().getGrammars().isEmpty()); + server.initialized(new InitializedParams()); + + Assertions.assertTrue(languageClient.clientLog.contains("logMessage - Info - Using app classpath")); + Assertions.assertTrue(languageClient.clientLog.contains("refreshCodeLenses")); + Assertions.assertTrue(languageClient.clientLog.contains("refreshDiagnostics")); + Assertions.assertTrue(languageClient.clientLog.contains("refreshInlayHints")); + Assertions.assertTrue(languageClient.clientLog.contains("refreshSemanticTokens")); + } + private void assertThrowsResponseError(ResponseErrorCode code, String message, Executable executable) { ResponseErrorException e = Assertions.assertThrows(ResponseErrorException.class, executable); @@ -162,39 +175,9 @@ private void assertThrowsResponseError(ResponseErrorCode code, String message, E Assertions.assertEquals(message, e.getMessage()); } - private LanguageClient newLanguageClient() + + private DummyLanguageClient newLanguageClient() { - return new LanguageClient() - { - @Override - public void telemetryEvent(Object object) - { - // Do nothing - } - - @Override - public void publishDiagnostics(PublishDiagnosticsParams diagnostics) - { - // Do nothing - } - - @Override - public void showMessage(MessageParams messageParams) - { - // Do nothing - } - - @Override - public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) - { - throw new UnsupportedOperationException(); - } - - @Override - public void logMessage(MessageParams message) - { - // Do nothing - } - }; + return new DummyLanguageClient(); } } diff --git a/pom.xml b/pom.xml index 814d26b5..95f091a9 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,10 @@ 5.10.1 2.0.10 + 2.15.1 + 3.2.0 + 3.4.2 + 0.8.10 3.3.0 @@ -135,6 +139,9 @@ false ${project.parent.basedir}/legend-engine-ide-lsp-test-reports/surefire-reports-aggregate + + ${maven.home} + @@ -326,6 +333,30 @@ + + commons-io + commons-io + ${commons-io.version} + + + + org.apache.maven.shared + maven-invoker + ${maven.invoker.version} + + + javax.inject + javax.inject + + + + + + org.apache.maven.shared + maven-shared-utils + ${maven.shared.utils.version} + + org.junit