From 795cfbc4f0f4fd0a7dbc8439ab6f473a59a50881 Mon Sep 17 00:00:00 2001 From: azerr Date: Mon, 9 Oct 2023 17:20:07 +0200 Subject: [PATCH] perf: Improve performance of Quarkus deployment jar support Fixes #1143 Signed-off-by: azerr --- .../ClasspathResourceChangedManager.java | 14 +- .../ClasspathResourceChangedNotifier.java | 25 +-- .../intellij/quarkus/QuarkusModuleUtil.java | 35 ++-- .../quarkus/QuarkusProjectService.java | 170 +++++++----------- .../deployment/ProgressIndicatorWrapper.java | 129 +++++++++++++ .../deployment/QuarkusDeploymentSupport.java | 167 +++++++++++++++++ .../quarkus/lsp/QuarkusLanguageClient.java | 29 ++- .../quarkus/maven/MavenToolDelegate.java | 57 +++--- .../intellij/quarkus/tool/ToolDelegate.java | 17 ++ src/main/resources/META-INF/plugin.xml | 1 + .../devtools/intellij/GradleTestCase.java | 6 +- 11 files changed, 479 insertions(+), 171 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/deployment/ProgressIndicatorWrapper.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/deployment/QuarkusDeploymentSupport.java diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedManager.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedManager.java index fbff7dff7..e840d5a07 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedManager.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedManager.java @@ -69,13 +69,10 @@ public interface Listener { private final Project project; - private final List preprocessors; - public ClasspathResourceChangedManager(Project project) { this.project = project; - this.preprocessors = new ArrayList<>(); // Send source files changed in debounce mode - this.resourceChangedNotifier = new ClasspathResourceChangedNotifier(project, preprocessors); + this.resourceChangedNotifier = new ClasspathResourceChangedNotifier(project); listener = new ClasspathResourceChangedListener(this); projectConnection = project.getMessageBus().connect(); // Track end of Java libraries update @@ -105,13 +102,4 @@ Project getProject() { ClasspathResourceChangedNotifier getResourceChangedNotifier() { return resourceChangedNotifier; } - - /** - * Add a preprocessor to update classpatch when a library changed before sending the {@link Listener#librariesChanged()} event. - * - * @param preprocessor the preprocessor to add. - */ - public void addPreprocessor(RunnableProgress preprocessor) { - preprocessors.add(preprocessor); - } } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedNotifier.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedNotifier.java index a2cac68c1..dfb955f6a 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedNotifier.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedNotifier.java @@ -14,11 +14,9 @@ package com.redhat.devtools.intellij.lsp4mp4ij.classpath; import com.intellij.openapi.Disposable; -import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.module.Module; -import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; @@ -27,10 +25,10 @@ import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -import java.util.stream.Collectors; +import java.util.HashSet; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; /** * Source file change notifier with a debounce mode. @@ -46,12 +44,10 @@ public class ClasspathResourceChangedNotifier implements Disposable { private final Set> sourceFiles; private boolean librariesChanged; - private final List processBeforeLibrariesChanged; private boolean disposed; - public ClasspathResourceChangedNotifier(Project project, List preprocessors) { + public ClasspathResourceChangedNotifier(Project project) { this.project = project; - this.processBeforeLibrariesChanged = preprocessors; sourceFiles = new HashSet<>(); } @@ -107,11 +103,7 @@ private void notifyChanges() { } if (librariesChanged) { // Java Libraries has changed - if (processBeforeLibrariesChanged.isEmpty() || ApplicationManager.getApplication().isUnitTestMode()) { - // No preprocessor or Test context, send directly the librariesChanged event. - for (var runnable : processBeforeLibrariesChanged) { - runnable.run(new EmptyProgressIndicator()); - } + if (ApplicationManager.getApplication().isUnitTestMode()) { // Send the libraries changed event project.getMessageBus().syncPublisher(ClasspathResourceChangedManager.TOPIC).librariesChanged(); librariesChanged = false; @@ -125,9 +117,6 @@ public void run(@NotNull ProgressIndicator progressIndicator) { // Execute preprocessor progressIndicator.setIndeterminate(false); progressIndicator.checkCanceled(); - for (var runnable : processBeforeLibrariesChanged) { - runnable.run(progressIndicator); - } } finally { // Send the libraries changed event project.getMessageBus().syncPublisher(ClasspathResourceChangedManager.TOPIC).librariesChanged(); @@ -152,7 +141,7 @@ public void dispose() { this.disposed = true; if (debounceTask != null) { debounceTask.cancel(); - debounceTask =null; + debounceTask = null; } if (debounceTimer != null) { debounceTimer.cancel(); diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusModuleUtil.java b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusModuleUtil.java index 82edd8c72..b7a3c7fbe 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusModuleUtil.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusModuleUtil.java @@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.text.MessageFormat; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -65,7 +66,7 @@ public static boolean isQuarkusExtensionWithDeploymentArtifact(Library library) if (library != null) { VirtualFile[] files = library.getFiles(OrderRootType.CLASSES); - for(int i=0; !result && i < files.length;++i) { + for (int i = 0; !result && i < files.length; ++i) { if (files[i].isDirectory()) { result = ToolDelegate.getDeploymentJarId(VfsUtilCore.virtualToIoFile(files[i])) != null; } @@ -77,7 +78,7 @@ public static boolean isQuarkusExtensionWithDeploymentArtifact(Library library) /** * Check if the Quarkus library needs to be recomputed and update it if required. * - * @param module the module to check + * @param module the module to check * @param progressIndicator */ public static void ensureQuarkusLibrary(Module module, ProgressIndicator progressIndicator) { @@ -95,20 +96,28 @@ public static void ensureQuarkusLibrary(Module module, ProgressIndicator progres Integer actualHash = computeHash(module); var qlib = OrderEntryUtil.findLibraryOrderEntry(ModuleRootManager.getInstance(module), QuarkusConstants.QUARKUS_DEPLOYMENT_LIBRARY_NAME); if (qlib == null || (actualHash != null && !actualHash.equals(previousHash)) || - !QuarkusConstants.QUARKUS_DEPLOYMENT_LIBRARY_VERSION.equals(component.getVersion())){ + !QuarkusConstants.QUARKUS_DEPLOYMENT_LIBRARY_VERSION.equals(component.getVersion())) { ModuleRootModificationUtil.updateModel(module, model -> { LibraryTable table = model.getModuleLibraryTable(); Library library = table.getLibraryByName(QuarkusConstants.QUARKUS_DEPLOYMENT_LIBRARY_NAME); while (library != null) { table.removeLibrary(library); - TelemetryService.instance().action(TelemetryService.MODEL_PREFIX + "removeLibrary"); + try { + TelemetryService.instance().action(TelemetryService.MODEL_PREFIX + "removeLibrary"); + } catch (Exception e) { + + } library = table.getLibraryByName(QuarkusConstants.QUARKUS_DEPLOYMENT_LIBRARY_NAME); } progressIndicator.checkCanceled(); - progressIndicator.setText("Collecting Quarkus deployment dependencies..."); + progressIndicator.setText("Adding in ''" + module.getName() + "'' Quarkus deployment dependencies to classpath..."); List[] files = toolDelegate.getDeploymentFiles(module, progressIndicator); LOGGER.info("Adding library to " + module.getName() + " previousHash=" + previousHash + " newHash=" + actualHash); - TelemetryService.instance().action(TelemetryService.MODEL_PREFIX + "addLibrary").send(); + try { + TelemetryService.instance().action(TelemetryService.MODEL_PREFIX + "addLibrary").send(); + } catch (Exception e) { + + } addLibrary(model, files, module); }); component.setHash(actualHash); @@ -120,7 +129,7 @@ public static void ensureQuarkusLibrary(Module module, ProgressIndicator progres } private static void addLibrary(ModifiableRootModel model, List[] files, Module module) { - LibraryEx library = (LibraryEx)model.getModuleLibraryTable().createLibrary(QuarkusConstants.QUARKUS_DEPLOYMENT_LIBRARY_NAME); + LibraryEx library = (LibraryEx) model.getModuleLibraryTable().createLibrary(QuarkusConstants.QUARKUS_DEPLOYMENT_LIBRARY_NAME); LibraryEx.ModifiableModelEx libraryModel = library.getModifiableModel(); for (VirtualFile rootFile : files[ToolDelegate.BINARY]) { @@ -145,14 +154,14 @@ private static Integer computeHash(Module module) { @Override public Set visitLibraryOrderEntry(@NotNull LibraryOrderEntry libraryOrderEntry, Set value) { if (!isQuarkusDeploymentLibrary(libraryOrderEntry) && isQuarkusExtensionWithDeploymentArtifact(libraryOrderEntry.getLibrary())) { - for(VirtualFile file : libraryOrderEntry.getFiles(OrderRootType.CLASSES)) { + for (VirtualFile file : libraryOrderEntry.getFiles(OrderRootType.CLASSES)) { value.add(file.getPath()); } } return value; } }, new HashSet<>()); - return files.isEmpty()?null:files.hashCode(); + return files.isEmpty() ? null : files.hashCode(); } /** @@ -174,7 +183,7 @@ public Boolean visitLibraryOrderEntry(@NotNull LibraryOrderEntry libraryOrderEnt } public static boolean isQuarkusLibrary(@NotNull LibraryOrderEntry libraryOrderEntry) { - return libraryOrderEntry.getLibraryName() != null && + return libraryOrderEntry.getLibraryName() != null && libraryOrderEntry.getLibraryName().contains(QuarkusConstants.QUARKUS_CORE_PREFIX); } @@ -204,9 +213,9 @@ public static boolean checkQuarkusVersion(Module module, Predicate pred .findFirst(); if (quarkusCoreJar.isPresent()) { Matcher quarkusCoreArtifactMatcher = QUARKUS_CORE_PATTERN.matcher(quarkusCoreJar.get().getName()); - if(quarkusCoreArtifactMatcher.matches()) { + if (quarkusCoreArtifactMatcher.matches()) { String quarkusVersion = quarkusCoreArtifactMatcher.group(1); - LOGGER.debug("Detected Quarkus version = " + quarkusVersion); + LOGGER.debug("Detected Quarkus version = " + quarkusVersion); Matcher quarkusVersionMatcher = QUARKUS_STANDARD_VERSIONING.matcher(quarkusVersion); return predicate.test(quarkusVersionMatcher); } else { @@ -219,7 +228,7 @@ public static boolean checkQuarkusVersion(Module module, Predicate pred public static Set getModulesURIs(Project project) { Set uris = new HashSet<>(); - for(Module module : ModuleManager.getInstance(project).getModules()) { + for (Module module : ModuleManager.getInstance(project).getModules()) { uris.add(PsiUtilsLSImpl.getProjectURI(module)); } return uris; diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusProjectService.java b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusProjectService.java index 97636622c..32a8649a5 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusProjectService.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusProjectService.java @@ -10,17 +10,14 @@ ******************************************************************************/ package com.redhat.devtools.intellij.quarkus; -import com.intellij.ProjectTopics; import com.intellij.json.JsonFileType; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.DumbService; -import com.intellij.openapi.project.ModuleListener; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; @@ -35,7 +32,6 @@ import org.eclipse.lsp4mp.commons.MicroProfileProjectInfo; import org.eclipse.lsp4mp.commons.MicroProfilePropertiesScope; import org.eclipse.lsp4mp.utils.JSONSchemaUtils; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,112 +40,84 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -public class QuarkusProjectService implements ClasspathResourceChangedManager.Listener, Disposable, ModuleListener { - private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusProjectService.class); +public class QuarkusProjectService implements ClasspathResourceChangedManager.Listener, Disposable { + private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusProjectService.class); - private final Map> schemas = new ConcurrentHashMap<>(); + private final Map> schemas = new ConcurrentHashMap<>(); - public static QuarkusProjectService getInstance(Project project) { - return ServiceManager.getService(project, QuarkusProjectService.class); - } + public static QuarkusProjectService getInstance(Project project) { + return ServiceManager.getService(project, QuarkusProjectService.class); + } - private final Project project; + private final MessageBusConnection connection; - private final MessageBusConnection connection; + public QuarkusProjectService(Project project) { + connection = project.getMessageBus().connect(); + connection.subscribe(ClasspathResourceChangedManager.TOPIC, this); + } - public QuarkusProjectService(Project project) { - this.project = project; - connection = project.getMessageBus().connect(); - connection.subscribe(ClasspathResourceChangedManager.TOPIC, this); - connection.subscribe(ProjectTopics.MODULES, this); - // Add the Quarkus deployment preprocessor - ClasspathResourceChangedManager.getInstance(project) - .addPreprocessor( - (progressIndicator) -> { - processModules(progressIndicator); - }); - } - @Override - public void moduleAdded(@NotNull Project project, @NotNull Module module) { - QuarkusModuleUtil.ensureQuarkusLibrary(module, new EmptyProgressIndicator()); - } - - public VirtualFile getSchema(Module module) { - var schemaEntry = schemas.get(module); - if (schemaEntry == null || !schemaEntry.getRight()) { - VirtualFile file = computeSchema(module, schemaEntry != null ? schemaEntry.getLeft() : null); - if (file != null) { - if (schemaEntry != null) { - schemaEntry.setRight(Boolean.TRUE); - } else { - schemaEntry = new MutablePair<>(file, Boolean.TRUE); - schemas.put(module, schemaEntry); - } - } - } - return schemaEntry != null ? schemaEntry.getLeft() : null; - } - - private static VirtualFile createJSONSchemaFile(String name) throws IOException { - return new LightVirtualFile(name + "-schema.json", JsonFileType.INSTANCE, ""); - } - - private VirtualFile computeSchema(Module module, VirtualFile schemaFile) { - try { - if (schemaFile == null) { - schemaFile = createJSONSchemaFile(module.getName()); - } - final VirtualFile schemaFile1 = schemaFile; - DumbService.getInstance(module.getProject()).runWhenSmart(() -> { - ApplicationManager.getApplication().runWriteAction(() -> { - try { - MicroProfileProjectInfo info = PropertiesManager.getInstance().getMicroProfileProjectInfo(module, - MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES, ClasspathKind.TEST, PsiUtilsLSImpl.getInstance(module.getProject()), - DocumentFormat.Markdown, new EmptyProgressIndicator()); - String schema = JSONSchemaUtils.toJSONSchema(info, false); - VfsUtil.saveText(schemaFile1, schema); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - }); - }); - return schemaFile; - } catch (IOException | ProcessCanceledException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - return null; - } - - @Override - public void librariesChanged() { - // Update the JSON schema cache - schemas.forEach((module, pair) -> { - pair.setRight(Boolean.FALSE); - }); - } - - @Override - public void sourceFilesChanged(Set> sources) { - sources.forEach(pair -> schemas.computeIfPresent(pair.getSecond(), (m, p) -> { - p.setRight(Boolean.FALSE); - return p; - })); - } - - public void processModules(com.intellij.openapi.progress.ProgressIndicator progressIndicator) { - if (!project.isDisposed()) { - for (var module : ModuleManager.getInstance(project).getModules()) { - LOGGER.info("Calling ensure from processModules"); - QuarkusModuleUtil.ensureQuarkusLibrary(module, progressIndicator); + public VirtualFile getSchema(Module module) { + var schemaEntry = schemas.get(module); + if (schemaEntry == null || !schemaEntry.getRight()) { + VirtualFile file = computeSchema(module, schemaEntry != null ? schemaEntry.getLeft() : null); + if (file != null) { + if (schemaEntry != null) { + schemaEntry.setRight(Boolean.TRUE); + } else { + schemaEntry = new MutablePair<>(file, Boolean.TRUE); + schemas.put(module, schemaEntry); + } } } - } + return schemaEntry != null ? schemaEntry.getLeft() : null; + } + private static VirtualFile createJSONSchemaFile(String name) throws IOException { + return new LightVirtualFile(name + "-schema.json", JsonFileType.INSTANCE, ""); + } - @Override - public void dispose() { - connection.disconnect(); - } + private VirtualFile computeSchema(Module module, VirtualFile schemaFile) { + try { + if (schemaFile == null) { + schemaFile = createJSONSchemaFile(module.getName()); + } + final VirtualFile schemaFile1 = schemaFile; + DumbService.getInstance(module.getProject()).runWhenSmart(() -> ApplicationManager.getApplication().runWriteAction(() -> { + try { + MicroProfileProjectInfo info = PropertiesManager.getInstance().getMicroProfileProjectInfo(module, + MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES, ClasspathKind.TEST, PsiUtilsLSImpl.getInstance(module.getProject()), + DocumentFormat.Markdown, new EmptyProgressIndicator()); + String schema = JSONSchemaUtils.toJSONSchema(info, false); + VfsUtil.saveText(schemaFile1, schema); + } catch (IOException e) { + LOGGER.warn(e.getLocalizedMessage(), e); + } + })); + return schemaFile; + } catch (IOException | ProcessCanceledException e) { + LOGGER.warn(e.getLocalizedMessage(), e); + } + return null; + } + + @Override + public void librariesChanged() { + // Update the JSON schema cache + schemas.forEach((module, pair) -> pair.setRight(Boolean.FALSE)); + } + + @Override + public void sourceFilesChanged(Set> sources) { + sources.forEach(pair -> schemas.computeIfPresent(pair.getSecond(), (m, p) -> { + p.setRight(Boolean.FALSE); + return p; + })); + } + + @Override + public void dispose() { + connection.disconnect(); + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/deployment/ProgressIndicatorWrapper.java b/src/main/java/com/redhat/devtools/intellij/quarkus/deployment/ProgressIndicatorWrapper.java new file mode 100644 index 000000000..03e8663bd --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/deployment/ProgressIndicatorWrapper.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2023 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 + ******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.deployment; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.impl.CoreProgressManager; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.NlsContexts; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Progress indicator wrapper. + */ +public class ProgressIndicatorWrapper implements ProgressIndicator { + + private final ProgressIndicator progressIndicator; + + public ProgressIndicatorWrapper(ProgressIndicator progressIndicator) { + this.progressIndicator = progressIndicator; + + } + public void start() { + progressIndicator.start(); + } + + public void stop() { + progressIndicator.stop(); + } + + public boolean isRunning() { + return progressIndicator.isRunning(); + } + + public void cancel() { + progressIndicator.cancel(); + } + + public boolean isCanceled() { + return progressIndicator.isCanceled(); + } + + public void setText(@NlsContexts.ProgressText String text) { + progressIndicator.setText(text); + } + + @NlsContexts.ProgressText + public String getText() { + return progressIndicator.getText(); + } + + public void setText2(@NlsContexts.ProgressDetails String text) { + progressIndicator.setText2(text); + } + + @NlsContexts.ProgressDetails + public String getText2() { + return progressIndicator.getText2(); + } + + public double getFraction() { + return progressIndicator.getFraction(); + } + + public void setFraction(double fraction) { + progressIndicator.setFraction(fraction); + } + + public void pushState() { + progressIndicator.pushState(); + } + + public void popState() { + progressIndicator.popState(); + } + + public boolean isModal() { + return progressIndicator.isModal(); + } + + public @NotNull ModalityState getModalityState() { + return progressIndicator.getModalityState(); + } + + public void setModalityProgress(@Nullable ProgressIndicator modalityProgress) { + progressIndicator.setModalityProgress(modalityProgress); + } + + public boolean isIndeterminate() { + return progressIndicator.isIndeterminate(); + } + + public void setIndeterminate(boolean indeterminate) { + progressIndicator.setIndeterminate(indeterminate); + } + + public void checkCanceled() throws ProcessCanceledException { + progressIndicator.checkCanceled(); + if (isCanceled() /*&& isCancelable()*/) { + Throwable trace = getCancellationTrace(); + throw trace instanceof ProcessCanceledException ? (ProcessCanceledException)trace : new ProcessCanceledException(trace); + } + } + + protected @Nullable Throwable getCancellationTrace() { + return this instanceof Disposable ? Disposer.getDisposalTrace((Disposable)this) : null; + } + + public boolean isPopupWasShown() { + return progressIndicator.isPopupWasShown(); + } + + public boolean isShowing() { + return progressIndicator.isShowing(); + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/deployment/QuarkusDeploymentSupport.java b/src/main/java/com/redhat/devtools/intellij/quarkus/deployment/QuarkusDeploymentSupport.java new file mode 100644 index 000000000..d1644cc2f --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/deployment/QuarkusDeploymentSupport.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * Copyright (c) 2023 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 + ******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.deployment; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.*; +import com.intellij.openapi.roots.impl.libraries.LibraryEx; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.messages.MessageBusConnection; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.lsp4mp4ij.classpath.ClasspathResourceChangedManager; +import com.redhat.devtools.intellij.quarkus.QuarkusConstants; +import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; +import com.redhat.devtools.intellij.quarkus.lsp.QuarkusServer; +import com.redhat.devtools.intellij.quarkus.tool.ToolDelegate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.idea.maven.project.MavenProjectsManager; +import org.jetbrains.idea.maven.project.MavenProjectsTree; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +/** + * Quarkus deployment support. + */ +public class QuarkusDeploymentSupport implements ClasspathResourceChangedManager.Listener, Disposable { + + private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusDeploymentSupport.class); + private static final Key> QUARKUS_DEPLOYMENT_LOADER_KEY = new Key<>(QuarkusDeploymentSupport.class.getName()); + + private final MessageBusConnection connection; + private final Project project; + + public static QuarkusDeploymentSupport getInstance(Project project) { + return ServiceManager.getService(project, QuarkusDeploymentSupport.class); + } + + public QuarkusDeploymentSupport(Project project) { + this.project = project; + connection = project.getMessageBus().connect(project); + connection.subscribe(ClasspathResourceChangedManager.TOPIC, this); + } + + public CompletableFuture updateClasspathWithQuarkusDeployment(Module module) { + CompletableFuture loader = module.getUserData(QUARKUS_DEPLOYMENT_LOADER_KEY); + if (isOutOfDated(loader)) { + return updateClasspathWithQuarkusDeploymentSync(module); + } + return loader; + } + + private static boolean isOutOfDated(CompletableFuture loader) { + return loader == null /*|| loader.isCancelled() || loader.isCompletedExceptionally()*/; + } + + @NotNull + private synchronized static CompletableFuture updateClasspathWithQuarkusDeploymentSync(Module module) { + CompletableFuture loader = module.getUserData(QUARKUS_DEPLOYMENT_LOADER_KEY); + if (!isOutOfDated(loader)) { + return loader; + } + var project = module.getProject(); + final CompletableFuture future = new CompletableFuture<>(); + CompletableFuture.runAsync(() -> { + Runnable task = () -> ProgressManager.getInstance().run(new Task.Backgroundable(project, "Adding Quarkus deployment dependencies to classpath...") { + @Override + public void run(@NotNull ProgressIndicator indicator) { + try { + long start = System.currentTimeMillis(); + ProgressIndicator wrappedIndicator = new ProgressIndicatorWrapper(indicator) { + + @Override + public boolean isCanceled() { + return super.isCanceled() || future.isCancelled(); + } + }; + QuarkusModuleUtil.ensureQuarkusLibrary(module, wrappedIndicator); + long elapsed = System.currentTimeMillis() - start; + LOGGER.info("Ensured QuarkusLibrary in " + elapsed + " ms"); + future.complete(null); + } catch (CancellationException | ProcessCanceledException e) { + future.cancel(true); + } catch (Exception e) { + LOGGER.error("Error while adding Quarkus deployment dependencies to classpath in '" + module.getName() + "'", e); + future.completeExceptionally(e); + } + } + }); + if (DumbService.getInstance(project).isDumb()) { + DumbService.getInstance(project).runWhenSmart(task); + } else { + task.run(); + } + }); + module.putUserData(QUARKUS_DEPLOYMENT_LOADER_KEY, future); + return future; + } + + @Override + public void dispose() { + connection.dispose(); + cancelFutures(); + } + + @Override + public void librariesChanged() { + cancelFutures(); + } + + private void cancelFutures() { + if (!project.isDisposed()) { + for (var module : ModuleManager.getInstance(project).getModules()) { + CompletableFuture loader = module.getUserData(QUARKUS_DEPLOYMENT_LOADER_KEY); + if (loader != null) { + loader.cancel(true); + module.putUserData(QUARKUS_DEPLOYMENT_LOADER_KEY, null); + } + } + } + } + + @Override + public void sourceFilesChanged(Set> sources) { + // Do nothing + } + + public void updateClasspathSync() { + var modules = ModuleManager.getInstance(project).getModules(); + for (var module : modules) { + try { + updateClasspathWithQuarkusDeployment(module).get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java index 282cc2e35..e96476ea5 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java @@ -33,6 +33,7 @@ import com.redhat.devtools.intellij.lsp4mp4ij.settings.MicroProfileInspectionsInfo; import com.redhat.devtools.intellij.lsp4mp4ij.settings.UserDefinedMicroProfileSettings; import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; +import com.redhat.devtools.intellij.quarkus.deployment.QuarkusDeploymentSupport; import org.eclipse.lsp4j.*; import org.eclipse.lsp4mp.commons.*; import org.eclipse.lsp4mp.commons.codeaction.CodeActionResolveData; @@ -41,6 +42,7 @@ import org.eclipse.lsp4mp.ls.api.MicroProfileLanguageServerAPI; import org.jetbrains.annotations.NotNull; +import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -57,6 +59,9 @@ public class QuarkusLanguageClient extends IndexAwareLanguageClient implements M public QuarkusLanguageClient(Project project) { super(project); + // Call Quarkus deployment support here to react on library changed (to evict quarkus deploiement cache) before + // sending an LSP microprofile/propertiesChanged notifications + QuarkusDeploymentSupport.getInstance(project); connection = project.getMessageBus().connect(project); connection.subscribe(ClasspathResourceChangedManager.TOPIC, this); inspectionsInfo = MicroProfileInspectionsInfo.getMicroProfileInspectionInfo(project); @@ -144,11 +149,31 @@ private boolean isConfigSource(VirtualFile file) { @Override public CompletableFuture getProjectInfo(MicroProfileProjectInfoParams params) { + IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject()); + VirtualFile file = null; + try { + file = utils.findFile(params.getUri()); + } catch (IOException e) { + throw new RuntimeException(e); + } + Module module = utils.getModule(file); + if (module == null) { + throw new RuntimeException(); + } + CompletableFuture loader = QuarkusDeploymentSupport.getInstance(getProject()).updateClasspathWithQuarkusDeployment(module); + if (loader.isDone()) { + return internalGetProjectInfo(params); + } + return loader + .thenCompose(unused -> internalGetProjectInfo(params)); + } + + private CompletableFuture internalGetProjectInfo(MicroProfileProjectInfoParams params) { var coalesceBy = new CoalesceByKey("microprofile/projectInfo", params.getUri(), params.getScopes()); String filePath = getFilePath(params.getUri()); return runAsBackground("Computing MicroProfile properties for '" + filePath + "'.", monitor -> - PropertiesManager.getInstance().getMicroProfileProjectInfo(params, PsiUtilsLSImpl.getInstance(getProject()), monitor) - , coalesceBy); + PropertiesManager.getInstance().getMicroProfileProjectInfo(params, PsiUtilsLSImpl.getInstance(getProject()), monitor) + , coalesceBy); } @Override diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/maven/MavenToolDelegate.java b/src/main/java/com/redhat/devtools/intellij/quarkus/maven/MavenToolDelegate.java index d3b72a53e..65d3224a3 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/maven/MavenToolDelegate.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/maven/MavenToolDelegate.java @@ -31,7 +31,6 @@ import org.jetbrains.idea.maven.project.MavenProject; import org.jetbrains.idea.maven.project.MavenProjectsManager; import org.jetbrains.idea.maven.server.MavenEmbedderWrapper; -import org.jetbrains.idea.maven.server.MavenServerManager; import org.jetbrains.idea.maven.utils.MavenProcessCanceledException; import org.jetbrains.idea.maven.utils.MavenUtil; import org.slf4j.Logger; @@ -39,12 +38,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.*; public class MavenToolDelegate implements ToolDelegate { private static final Logger LOGGER = LoggerFactory.getLogger(MavenToolDelegate.class); @@ -101,7 +95,7 @@ private void getDeploymentFiles(Module module, MavenProject mavenProject, List binaryDependencies = ensureDownloaded(module, mavenProject, toDownload, null); + progressIndicator.checkCanceled(); + progressIndicator.setText2("Collecting Quarkus deployment dependencies from the '" + toDownload.size() + "' binary dependencies"); + Set binaryDependencies = resolveDeploymentArtifacts(module, mavenProject, toDownload, null, progressIndicator); i = counter / binaryDependencies.size(); toDownload.clear(); for (MavenArtifact binaryDependency : binaryDependencies) { progressIndicator.checkCanceled(); - progressIndicator.setText2("Searching deployment descriptor in '" + binaryDependency.getArtifactId() + "'"); + progressIndicator.setText2("Searching deployment descriptor in '" + binaryDependency.getArtifactId() + "' binary"); if (!"test".equals(binaryDependency.getScope())) { if (processDependency(mavenProject, result, downloaded, binaryDependency, BINARY)) { toDownload.add(binaryDependency.getMavenId()); @@ -132,11 +128,13 @@ private void getDeploymentFiles(Module module, MavenProject mavenProject, List sourcesDependencies = ensureDownloaded(module, mavenProject, toDownload, "sources"); + progressIndicator.checkCanceled(); + progressIndicator.setText2("Collecting Quarkus deployment dependencies from the '" + toDownload.size() + "' source dependencies"); + Set sourcesDependencies = resolveDeploymentArtifacts(module, mavenProject, toDownload, "sources", progressIndicator); i = counter / sourcesDependencies.size(); for (MavenArtifact sourceDependency : sourcesDependencies) { progressIndicator.checkCanceled(); - progressIndicator.setText2("Searching deployment descriptor in '" + sourceDependency.getArtifactId() + "'"); + progressIndicator.setText2("Searching deployment descriptor in '" + sourceDependency.getArtifactId() + "' source"); processDependency(mavenProject, result, downloaded, sourceDependency, SOURCES); p+=i; progressIndicator.setFraction(p); @@ -145,7 +143,6 @@ private void getDeploymentFiles(Module module, MavenProject mavenProject, List[] result, Set downloaded, MavenArtifact dependency, int type) { boolean added = false; - if (mavenProject.findDependencies(dependency.getGroupId(), dependency.getArtifactId()).isEmpty() && !downloaded.contains(dependency)) { downloaded.add(dependency); VirtualFile jarRoot = getJarFile(dependency.getFile()); @@ -157,23 +154,43 @@ private boolean processDependency(MavenProject mavenProject, List[] return added; } - private List ensureDownloaded(Module module, MavenProject mavenProject, Set deploymentIds, String classifier) { - List result = new ArrayList<>(); - long start = System.currentTimeMillis(); + private Set resolveDeploymentArtifacts(Module module, MavenProject mavenProject, Set deploymentIds, String classifier, ProgressIndicator progressIndicator) { + Set deploymentArtifacts = new HashSet<>(); try { MavenEmbedderWrapper serverWrapper = createEmbedderWrapper(module.getProject(), mavenProject.getDirectory()); if (classifier != null) { for(MavenId id : deploymentIds) { - result.add(serverWrapper.resolve(new MavenArtifactInfo(id, "jar", classifier), mavenProject.getRemoteRepositories())); + deploymentArtifacts.add(serverWrapper.resolve(new MavenArtifactInfo(id, "jar", classifier), mavenProject.getRemoteRepositories())); } } else { - List infos = deploymentIds.stream().map(id -> new MavenArtifactInfo(id, "jar", classifier)).collect(Collectors.toList()); - result = serverWrapper.resolveTransitively(infos, mavenProject.getRemoteRepositories()); + for (var deploymentId : deploymentIds) { + boolean shouldResolveArtifactTransitively = ToolDelegate.shouldResolveArtifactTransitively(deploymentId); + progressIndicator.checkCanceled(); + progressIndicator.setText2("Resolving " + (shouldResolveArtifactTransitively ? " (Transitevely) " : "") + "'" + deploymentId + "'"); + if (shouldResolveArtifactTransitively) { + // Resolving the deployment artifact and their dependencies + List infos = Arrays.asList( new MavenArtifactInfo(deploymentId, "jar", classifier)); + List resolvedArtifacts = serverWrapper.resolveArtifactTransitively(infos, mavenProject.getRemoteRepositories()).mavenResolvedArtifacts; + for (var resolvedArtifact: resolvedArtifacts) { + addDeploymentArtifact(resolvedArtifact, deploymentArtifacts); + } + } else { + // Resolving only the deployment artifact + MavenArtifact resolvedArtifact = serverWrapper.resolve(new MavenArtifactInfo(deploymentId, "jar", classifier), mavenProject.getRemoteRepositories()); //.mavenResolvedArtifacts; + addDeploymentArtifact(resolvedArtifact, deploymentArtifacts); + } + } } } catch (MavenProcessCanceledException | RuntimeException e) { LOGGER.warn(e.getLocalizedMessage(), e); } - return result; + return deploymentArtifacts; + } + + private static void addDeploymentArtifact(MavenArtifact resolvedArtifact, Set result) { + if (resolvedArtifact != null && !result.contains(resolvedArtifact)) { + result.add(resolvedArtifact); + } } /** diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/tool/ToolDelegate.java b/src/main/java/com/redhat/devtools/intellij/quarkus/tool/ToolDelegate.java index 06b9fbe68..88fbcdfe7 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/tool/ToolDelegate.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/tool/ToolDelegate.java @@ -19,6 +19,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; import org.jetbrains.annotations.NotNull; +import org.jetbrains.idea.maven.model.MavenId; import java.io.File; import java.io.FileReader; @@ -36,6 +37,22 @@ import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.QUARKUS_EXTENSION_PROPERTIES; public interface ToolDelegate { + + static boolean shouldResolveArtifactTransitively(MavenId deploymentId) { + // The kubernetes support is only available if quarkus-kubernetes artifact is + // declared in the pom.xml + // When quarkus-kubernetes is declared, this JAR declares the deployment JAR + // quarkus-kubernetes-deployment + // This quarkus-kubernetes-deployment artifact has some dependencies to + // io.dekorate + + // In other words, to add + // io.dekorate.kubernetes.annotation.KubernetesApplication class in the search + // classpath, + // the dependencies of quarkus-kubernetes-deployment artifact must be downloaded + return "quarkus-kubernetes-deployment".equals(deploymentId.getArtifactId()) || "quarkus-openshift-deployment".equals(deploymentId.getArtifactId()); + } + static String getDeploymentJarId(File file) { String result = null; if (file.isDirectory()) { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ea4f68ef0..5ef647863 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -332,6 +332,7 @@ + diff --git a/src/test/java/com/redhat/devtools/intellij/GradleTestCase.java b/src/test/java/com/redhat/devtools/intellij/GradleTestCase.java index cf9852f5e..36bd07359 100644 --- a/src/test/java/com/redhat/devtools/intellij/GradleTestCase.java +++ b/src/test/java/com/redhat/devtools/intellij/GradleTestCase.java @@ -14,6 +14,7 @@ import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.progress.EmptyProgressIndicator; import com.redhat.devtools.intellij.quarkus.QuarkusProjectService; +import com.redhat.devtools.intellij.quarkus.deployment.QuarkusDeploymentSupport; import com.redhat.devtools.intellij.quarkus.gradle.AbstractGradleToolDelegate; import org.gradle.tooling.GradleConnector; import org.gradle.tooling.ProjectConnection; @@ -73,10 +74,7 @@ protected File getMavenRepository() { @Override protected void importProject() { super.importProject(); -// for(Module m : ModuleManager.getInstance(myProject).getModules()) { -// setupJdkForModule(m.getName()); -// } - QuarkusProjectService.getInstance(myProject).processModules(new EmptyProgressIndicator()); + QuarkusDeploymentSupport.getInstance(myProject).updateClasspathSync(); } @Parameterized.Parameters(name = "{index}: with Gradle-{0}")