From b7d98e501e4a9f594303fb240fb8bea8240644db Mon Sep 17 00:00:00 2001 From: nik Date: Thu, 27 Aug 2015 13:00:12 +0300 Subject: [PATCH] added Maven's implementation for ProjectModelModificationService so various 'add missing dependency' actions will modify pom.xml files in case of Maven projects --- ...ExternalLibraryToDependenciesQuickFix.java | 18 +- .../ProjectModelModificationService.java | 26 +-- .../openapi/roots/ProjectModelModifier.java | 43 ++++- .../roots/impl/IdeaProjectModelModifier.java | 17 +- .../ProjectModelModificationServiceImpl.java | 25 ++- .../importing/MavenProjectModelModifier.java | 165 ++++++++++++++++++ .../maven/project/MavenProjectsManager.java | 37 ++-- .../project/MavenProjectsManagerWatcher.java | 56 +++--- .../src/main/resources/META-INF/plugin.xml | 1 + .../MavenProjectModelModifierTest.java | 146 ++++++++++++++++ 10 files changed, 463 insertions(+), 71 deletions(-) create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/importing/MavenProjectModelModifier.java create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/actions/MavenProjectModelModifierTest.java diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExternalLibraryToDependenciesQuickFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExternalLibraryToDependenciesQuickFix.java index 71841d426b0e..3ae8e757d167 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExternalLibraryToDependenciesQuickFix.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExternalLibraryToDependenciesQuickFix.java @@ -15,6 +15,8 @@ */ package com.intellij.codeInsight.daemon.impl.quickfix; +import com.intellij.openapi.application.Result; +import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.roots.ExternalLibraryDescriptor; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; @@ -23,6 +25,7 @@ import com.intellij.openapi.roots.DependencyScope; import com.intellij.openapi.roots.ProjectModelModificationService; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; +import com.intellij.util.Consumer; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -66,9 +69,18 @@ class AddExternalLibraryToDependenciesQuickFix extends OrderEntryFix { } @Override - public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + public void invoke(@NotNull Project project, final Editor editor, PsiFile file) throws IncorrectOperationException { DependencyScope scope = suggestScopeByLocation(myCurrentModule, myReference.getElement()); - ProjectModelModificationService.getInstance(project).addDependency(myCurrentModule, myLibraryDescriptor, scope); - importClass(myCurrentModule, editor, myReference, myQualifiedClassName); + ProjectModelModificationService.getInstance(project).addDependency(myCurrentModule, myLibraryDescriptor, scope).done( + new Consumer() { + @Override + public void consume(Void aVoid) { + new WriteAction() { + protected void run(@NotNull final Result result) { + importClass(myCurrentModule, editor, myReference, myQualifiedClassName); + } + }.execute(); + } + }); } } diff --git a/java/java-impl/src/com/intellij/openapi/roots/ProjectModelModificationService.java b/java/java-impl/src/com/intellij/openapi/roots/ProjectModelModificationService.java index e842bc6a225a..357186c5ac90 100644 --- a/java/java-impl/src/com/intellij/openapi/roots/ProjectModelModificationService.java +++ b/java/java-impl/src/com/intellij/openapi/roots/ProjectModelModificationService.java @@ -20,11 +20,17 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.libraries.Library; import org.jetbrains.annotations.NotNull; +import org.jetbrains.concurrency.Promise; import java.util.Collection; import java.util.Collections; /** + * Provides methods to perform high-level modifications of project configuration accordingly with dependency management system used in the + * project. E.g. if the project is imported from Maven the methods will modify pom.xml files and invoke reimporting to update IDEA's + * project model. Since importing the changes to IDEA's project model may take a while the method work asynchronously and returns + * {@link Promise} objects which may be used to be notified when the project configuration is finally updated. + * * @author nik */ public abstract class ProjectModelModificationService { @@ -32,22 +38,22 @@ public abstract class ProjectModelModificationService { return ServiceManager.getService(project, ProjectModelModificationService.class); } - public void addDependency(@NotNull Module from, @NotNull Module to) { - addDependency(from, to, DependencyScope.COMPILE); + public Promise addDependency(@NotNull Module from, @NotNull Module to) { + return addDependency(from, to, DependencyScope.COMPILE); } - public abstract void addDependency(@NotNull Module from, @NotNull Module to, @NotNull DependencyScope scope); + public abstract Promise addDependency(@NotNull Module from, @NotNull Module to, @NotNull DependencyScope scope); - public void addDependency(@NotNull Module from, @NotNull ExternalLibraryDescriptor libraryDescriptor) { - addDependency(from, libraryDescriptor, DependencyScope.COMPILE); + public Promise addDependency(@NotNull Module from, @NotNull ExternalLibraryDescriptor libraryDescriptor) { + return addDependency(from, libraryDescriptor, DependencyScope.COMPILE); } - public void addDependency(Module from, ExternalLibraryDescriptor descriptor, DependencyScope scope) { - addDependency(Collections.singletonList(from), descriptor, scope); + public Promise addDependency(@NotNull Module from, @NotNull ExternalLibraryDescriptor descriptor, @NotNull DependencyScope scope) { + return addDependency(Collections.singletonList(from), descriptor, scope); } - public abstract void addDependency(@NotNull Collection from, @NotNull ExternalLibraryDescriptor libraryDescriptor, - @NotNull DependencyScope scope); + public abstract Promise addDependency(@NotNull Collection from, @NotNull ExternalLibraryDescriptor libraryDescriptor, + @NotNull DependencyScope scope); - public abstract void addDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope); + public abstract Promise addDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope); } diff --git a/java/java-impl/src/com/intellij/openapi/roots/ProjectModelModifier.java b/java/java-impl/src/com/intellij/openapi/roots/ProjectModelModifier.java index 398dfd0a6042..cc324064f529 100644 --- a/java/java-impl/src/com/intellij/openapi/roots/ProjectModelModifier.java +++ b/java/java-impl/src/com/intellij/openapi/roots/ProjectModelModifier.java @@ -19,20 +19,53 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.module.Module; import com.intellij.openapi.roots.libraries.Library; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.concurrency.Promise; import java.util.Collection; /** + * Register implementation of this extension to support custom dependency management system for {@link ProjectModelModificationService}. + * * @author nik */ public abstract class ProjectModelModifier { public static final ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.projectModelModifier"); - public abstract boolean addModuleDependency(@NotNull Module from, @NotNull Module to, @NotNull DependencyScope scope); + /** + * Implementation of this method should add dependency from module {@code from} to module {@code to} with scope {@code scope} accordingly + * to this dependencies management system. If it takes some time to propagate changes in the external project configuration to IDEA's + * project model the method may schedule this work for asynchronous execution and return {@link Promise} instance which will be fulfilled + * when the work is done. + * @return {@link Promise} instance if dependencies between these modules can be handled by this dependencies management system or + * {@code null} otherwise + */ + @Nullable + public abstract Promise addModuleDependency(@NotNull Module from, @NotNull Module to, @NotNull DependencyScope scope); - public abstract boolean addExternalLibraryDependency(@NotNull Collection modules, - @NotNull ExternalLibraryDescriptor descriptor, - @NotNull DependencyScope scope); + /** + * Implementation of this method should add dependency from modules {@code modules} to an external library with scope {@code scope} accordingly + * to this dependencies management system. If it takes some time to propagate changes in the external project configuration to IDEA's + * project model the method may schedule this work for asynchronous execution and return {@link Promise} instance which will be fulfilled + * when the work is done. + * + * @return {@link Promise} instance if dependencies of these modules can be handled by this dependencies management system or + * {@code null} otherwise + */ + @Nullable + public abstract Promise addExternalLibraryDependency(@NotNull Collection modules, + @NotNull ExternalLibraryDescriptor descriptor, + @NotNull DependencyScope scope); - public abstract boolean addLibraryDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope); + /** + * Implementation of this method should add dependency from module {@code from} to {@code library} with scope {@code scope} accordingly + * to this dependencies management system. If it takes some time to propagate changes in the external project configuration to IDEA's + * project model the method may schedule this work for asynchronous execution and return {@link Promise} instance which will be fulfilled + * when the work is done. + * + * @return {@link Promise} instance if dependencies between these modules can be handled by this dependencies management system or + * {@code null} otherwise + */ + @Nullable + public abstract Promise addLibraryDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope); } diff --git a/java/java-impl/src/com/intellij/openapi/roots/impl/IdeaProjectModelModifier.java b/java/java-impl/src/com/intellij/openapi/roots/impl/IdeaProjectModelModifier.java index 981cc47599d6..3b3d4c9491b4 100644 --- a/java/java-impl/src/com/intellij/openapi/roots/impl/IdeaProjectModelModifier.java +++ b/java/java-impl/src/com/intellij/openapi/roots/impl/IdeaProjectModelModifier.java @@ -28,6 +28,7 @@ import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar; import com.intellij.openapi.roots.libraries.LibraryUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.concurrency.Promise; import java.util.Collection; import java.util.Collections; @@ -45,15 +46,15 @@ public class IdeaProjectModelModifier extends ProjectModelModifier { } @Override - public boolean addModuleDependency(@NotNull Module from, @NotNull Module to, @NotNull DependencyScope scope) { + public Promise addModuleDependency(@NotNull Module from, @NotNull Module to, @NotNull DependencyScope scope) { ModuleRootModificationUtil.addDependency(from, to, scope, false); - return true; + return Promise.DONE; } @Override - public boolean addExternalLibraryDependency(@NotNull final Collection modules, - @NotNull final ExternalLibraryDescriptor descriptor, - @NotNull final DependencyScope scope) { + public Promise addExternalLibraryDependency(@NotNull final Collection modules, + @NotNull final ExternalLibraryDescriptor descriptor, + @NotNull final DependencyScope scope) { List defaultRoots = descriptor.getLibraryClassesRoots(); Module firstModule = ContainerUtil.getFirstItem(modules); LOG.assertTrue(firstModule != null); @@ -82,12 +83,12 @@ public class IdeaProjectModelModifier extends ProjectModelModifier { }.execute(); } } - return true; + return Promise.DONE; } @Override - public boolean addLibraryDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope) { + public Promise addLibraryDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope) { OrderEntryUtil.addLibraryToRoots(from, library); - return true; + return Promise.DONE; } } diff --git a/java/java-impl/src/com/intellij/openapi/roots/impl/ProjectModelModificationServiceImpl.java b/java/java-impl/src/com/intellij/openapi/roots/impl/ProjectModelModificationServiceImpl.java index 5f6efd846384..067dee75fa0c 100644 --- a/java/java-impl/src/com/intellij/openapi/roots/impl/ProjectModelModificationServiceImpl.java +++ b/java/java-impl/src/com/intellij/openapi/roots/impl/ProjectModelModificationServiceImpl.java @@ -23,6 +23,7 @@ import com.intellij.openapi.roots.ProjectModelModificationService; import com.intellij.openapi.roots.ProjectModelModifier; import com.intellij.openapi.roots.libraries.Library; import org.jetbrains.annotations.NotNull; +import org.jetbrains.concurrency.Promise; import java.util.Collection; @@ -37,30 +38,36 @@ public class ProjectModelModificationServiceImpl extends ProjectModelModificatio } @Override - public void addDependency(@NotNull Module from, @NotNull Module to, @NotNull DependencyScope scope) { + public Promise addDependency(@NotNull Module from, @NotNull Module to, @NotNull DependencyScope scope) { for (ProjectModelModifier modifier : getModelModifiers()) { - if (modifier.addModuleDependency(from, to, scope)) { - return; + Promise promise = modifier.addModuleDependency(from, to, scope); + if (promise != null) { + return promise; } } + return Promise.REJECTED; } @Override - public void addDependency(@NotNull Collection from, @NotNull ExternalLibraryDescriptor libraryDescriptor, @NotNull DependencyScope scope) { + public Promise addDependency(@NotNull Collection from, @NotNull ExternalLibraryDescriptor libraryDescriptor, @NotNull DependencyScope scope) { for (ProjectModelModifier modifier : getModelModifiers()) { - if (modifier.addExternalLibraryDependency(from, libraryDescriptor, scope)) { - return; + Promise promise = modifier.addExternalLibraryDependency(from, libraryDescriptor, scope); + if (promise != null) { + return promise; } } + return Promise.REJECTED; } @Override - public void addDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope) { + public Promise addDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope) { for (ProjectModelModifier modifier : getModelModifiers()) { - if (modifier.addLibraryDependency(from, library, scope)) { - return; + Promise promise = modifier.addLibraryDependency(from, library, scope); + if (promise != null) { + return promise; } } + return Promise.REJECTED; } @NotNull diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/importing/MavenProjectModelModifier.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/importing/MavenProjectModelModifier.java new file mode 100644 index 000000000000..94da4a79449f --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/importing/MavenProjectModelModifier.java @@ -0,0 +1,165 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * 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.jetbrains.idea.maven.importing; + +import com.intellij.openapi.application.Result; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.DependencyScope; +import com.intellij.openapi.roots.ExternalLibraryDescriptor; +import com.intellij.openapi.roots.ProjectModelModifier; +import com.intellij.openapi.roots.libraries.Library; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.util.PsiUtilCore; +import com.intellij.psi.xml.XmlFile; +import com.intellij.util.text.VersionComparatorUtil; +import com.intellij.util.xml.DomUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.concurrency.Promise; +import org.jetbrains.idea.maven.dom.MavenDomUtil; +import org.jetbrains.idea.maven.dom.model.MavenDomDependency; +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel; +import org.jetbrains.idea.maven.indices.MavenProjectIndicesManager; +import org.jetbrains.idea.maven.model.MavenArtifact; +import org.jetbrains.idea.maven.model.MavenConstants; +import org.jetbrains.idea.maven.model.MavenId; +import org.jetbrains.idea.maven.project.MavenProject; +import org.jetbrains.idea.maven.project.MavenProjectsManager; +import org.jetbrains.idea.maven.utils.library.RepositoryAttachHandler; + +import java.util.*; + +/** + * @author nik + */ +public class MavenProjectModelModifier extends ProjectModelModifier { + private final Project myProject; + private final MavenProjectsManager myProjectsManager; + private MavenProjectIndicesManager myIndicesManager; + + public MavenProjectModelModifier(Project project, MavenProjectsManager projectsManager, MavenProjectIndicesManager manager) { + myProject = project; + myProjectsManager = projectsManager; + myIndicesManager = manager; + } + + @Nullable + @Override + public Promise addModuleDependency(@NotNull Module from, @NotNull Module to, @NotNull final DependencyScope scope) { + final MavenProject toProject = myProjectsManager.findProject(to); + if (toProject == null) return null; + MavenId mavenId = toProject.getMavenId(); + + return addDependency(Collections.singletonList(from), mavenId, scope); + } + + private Promise addDependency(@NotNull Collection fromModules, @NotNull final MavenId mavenId, @NotNull final DependencyScope scope) { + final List models = new ArrayList(fromModules.size()); + List files = new ArrayList(fromModules.size()); + List projectToUpdate = new ArrayList(fromModules.size()); + for (Module from : fromModules) { + if (!myProjectsManager.isMavenizedModule(from)) return null; + MavenProject fromProject = myProjectsManager.findProject(from); + if (fromProject == null) return null; + + final MavenDomProjectModel model = MavenDomUtil.getMavenDomProjectModel(myProject, fromProject.getFile()); + if (model == null) return null; + models.add(model); + files.add(DomUtil.getFile(model)); + projectToUpdate.add(fromProject); + } + + new WriteCommandAction(myProject, "Add Maven Dependency", PsiUtilCore.toPsiFileArray(files)) { + @Override + protected void run(@NotNull Result result) throws Throwable { + for (MavenDomProjectModel model : models) { + MavenDomDependency dependency = MavenDomUtil.createDomDependency(model, null, mavenId); + String mavenScope = getMavenScope(scope); + if (mavenScope != null) { + dependency.getScope().setStringValue(mavenScope); + } + Document document = PsiDocumentManager.getInstance(myProject).getDocument(DomUtil.getFile(model)); + if (document != null) { + FileDocumentManager.getInstance().saveDocument(document); + } + } + } + }.execute(); + return myProjectsManager.forceUpdateProjects(projectToUpdate); + } + + @Nullable + @Override + public Promise addExternalLibraryDependency(@NotNull Collection modules, + @NotNull ExternalLibraryDescriptor descriptor, + @NotNull DependencyScope scope) { + for (Module module : modules) { + if (!myProjectsManager.isMavenizedModule(module)) { + return null; + } + } + + String version = selectVersion(descriptor); + MavenId mavenId = new MavenId(descriptor.getLibraryGroupId(), descriptor.getLibraryArtifactId(), version); + return addDependency(modules, mavenId, scope); + } + + @NotNull + private String selectVersion(@NotNull ExternalLibraryDescriptor descriptor) { + Set versions = myIndicesManager.getVersions(descriptor.getLibraryGroupId(), descriptor.getLibraryArtifactId()); + List suitableVersions = new ArrayList(); + String minVersion = descriptor.getMinVersion(); + String maxVersion = descriptor.getMaxVersion(); + for (String version : versions) { + if ((minVersion == null || VersionComparatorUtil.compare(minVersion, version) <= 0) + && (maxVersion == null || VersionComparatorUtil.compare(version, maxVersion) < 0)) { + suitableVersions.add(version); + } + } + return Collections.max(suitableVersions, VersionComparatorUtil.COMPARATOR); + } + + @Nullable + @Override + public Promise addLibraryDependency(@NotNull Module from, @NotNull Library library, @NotNull DependencyScope scope) { + String name = library.getName(); + if (name != null && name.startsWith(MavenArtifact.MAVEN_LIB_PREFIX)) { + //it would be better to use RepositoryLibraryType for libraries imported from Maven and fetch mavenId from the library properties instead + String mavenCoordinates = StringUtil.trimStart(name, MavenArtifact.MAVEN_LIB_PREFIX); + return addDependency(Collections.singletonList(from), RepositoryAttachHandler.getMavenId(mavenCoordinates), scope); + } + return null; + } + + @Nullable + private static String getMavenScope(DependencyScope scope) { + switch (scope) { + case RUNTIME: + return MavenConstants.SCOPE_RUNTIME; + case TEST: + return MavenConstants.SCOPE_TEST; + case PROVIDED: + return MavenConstants.SCOPE_PROVIDED; + default: + return null; + } + } +} diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManager.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManager.java index c7a7dcccd20f..811f10df3b44 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManager.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManager.java @@ -53,6 +53,8 @@ import gnu.trove.THashSet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; +import org.jetbrains.concurrency.AsyncPromise; +import org.jetbrains.concurrency.Promise; import org.jetbrains.idea.maven.importing.MavenDefaultModifiableModelsProvider; import org.jetbrains.idea.maven.importing.MavenFoldersImporter; import org.jetbrains.idea.maven.importing.MavenModifiableModelsProvider; @@ -780,8 +782,8 @@ public class MavenProjectsManager extends MavenSimpleProjectComponent doScheduleUpdateProjects(null, false, forceImportAndResolve); } - public void forceUpdateProjects(@NotNull Collection projects) { - doScheduleUpdateProjects(projects, true, true); + public AsyncPromise forceUpdateProjects(@NotNull Collection projects) { + return doScheduleUpdateProjects(projects, true, true); } public void forceUpdateAllProjectsOrFindAllAvailablePomFiles() { @@ -791,30 +793,34 @@ public class MavenProjectsManager extends MavenSimpleProjectComponent doScheduleUpdateProjects(null, true, true); } - private void doScheduleUpdateProjects(final Collection projects, - final boolean forceUpdate, - final boolean forceImportAndResolve) { + private AsyncPromise doScheduleUpdateProjects(final Collection projects, + final boolean forceUpdate, + final boolean forceImportAndResolve) { + final AsyncPromise promise = new AsyncPromise(); MavenUtil.runWhenInitialized(myProject, new DumbAwareRunnable() { public void run() { if (projects == null) { - myWatcher.scheduleUpdateAll(forceUpdate, forceImportAndResolve); + myWatcher.scheduleUpdateAll(forceUpdate, forceImportAndResolve).processed(promise); } else { myWatcher.scheduleUpdate(MavenUtil.collectFiles(projects), Collections.emptyList(), forceUpdate, - forceImportAndResolve); + forceImportAndResolve).processed(promise); } } }); + return promise; } - public void scheduleImportAndResolve() { - scheduleResolve(); // scheduleImport will be called after the scheduleResolve process has finished + public Promise> scheduleImportAndResolve() { + AsyncPromise> promise = scheduleResolve();// scheduleImport will be called after the scheduleResolve process has finished fireImportAndResolveScheduled(); + return promise; } - private void scheduleResolve() { + private AsyncPromise> scheduleResolve() { + final AsyncPromise> result = new AsyncPromise>(); runWhenFullyOpen(new Runnable() { public void run() { LinkedHashSet toResolve; @@ -830,7 +836,9 @@ public class MavenProjectsManager extends MavenSimpleProjectComponent Runnable onCompletion = it.hasNext() ? null : new Runnable() { @Override public void run() { - if (hasScheduledProjects()) scheduleImport(); + if (hasScheduledProjects()) { + scheduleImport().processed(result); + } } }; @@ -839,6 +847,7 @@ public class MavenProjectsManager extends MavenSimpleProjectComponent } } }); + return result; } public void evaluateEffectivePom(@NotNull final MavenProject mavenProject, @NotNull final NullableConsumer consumer) { @@ -949,16 +958,18 @@ public class MavenProjectsManager extends MavenSimpleProjectComponent scheduleImport(); } - private void scheduleImport() { + private Promise> scheduleImport() { + final AsyncPromise> result = new AsyncPromise>(); runWhenFullyOpen(new Runnable() { public void run() { myImportingQueue.queue(new Update(MavenProjectsManager.this) { public void run() { - importProjects(); + result.setResult(importProjects()); } }); } }); + return result; } @TestOnly diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManagerWatcher.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManagerWatcher.java index b2ace1fb36eb..fb744ed57323 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManagerWatcher.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManagerWatcher.java @@ -42,6 +42,7 @@ import com.intellij.openapi.vfs.pointers.VirtualFilePointer; import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener; import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager; import com.intellij.psi.PsiDocumentManager; +import com.intellij.util.Consumer; import com.intellij.util.PathUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.messages.MessageBusConnection; @@ -50,6 +51,8 @@ import gnu.trove.THashSet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; +import org.jetbrains.concurrency.AsyncPromise; +import org.jetbrains.concurrency.Promise; import org.jetbrains.idea.maven.model.MavenConstants; import org.jetbrains.idea.maven.model.MavenExplicitProfiles; import org.jetbrains.idea.maven.utils.MavenMergingUpdateQueue; @@ -270,38 +273,45 @@ public class MavenProjectsManagerWatcher { scheduleUpdateAll(false, false); } - public void scheduleUpdateAll(boolean force, final boolean forceImportAndResolve) { - Runnable onCompletion = new Runnable() { - @Override - public void run() { - if (myProject.isDisposed()) return; - - if (forceImportAndResolve || myManager.getImportingSettings().isImportAutomatically()) { - myManager.scheduleImportAndResolve(); - } - } - }; + public Promise scheduleUpdateAll(boolean force, final boolean forceImportAndResolve) { + final AsyncPromise promise = new AsyncPromise(); + Runnable onCompletion = createScheduleImportAction(forceImportAndResolve, promise); myReadingProcessor.scheduleTask(new MavenProjectsProcessorReadingTask(force, myProjectsTree, myGeneralSettings, onCompletion)); + return promise; } - public void scheduleUpdate(List filesToUpdate, - List filesToDelete, - boolean force, - final boolean forceImportAndResolve) { - Runnable onCompletion = new Runnable() { - @Override - public void run() { - if (forceImportAndResolve || myManager.getImportingSettings().isImportAutomatically()) { - myManager.scheduleImportAndResolve(); - } - } - }; + public Promise scheduleUpdate(List filesToUpdate, + List filesToDelete, + boolean force, + final boolean forceImportAndResolve) { + final AsyncPromise promise = new AsyncPromise(); + Runnable onCompletion = createScheduleImportAction(forceImportAndResolve, promise); myReadingProcessor.scheduleTask(new MavenProjectsProcessorReadingTask(filesToUpdate, filesToDelete, force, myProjectsTree, myGeneralSettings, onCompletion)); + return promise; + } + + @NotNull + private Runnable createScheduleImportAction(final boolean forceImportAndResolve, final AsyncPromise promise) { + return new Runnable() { + @Override + public void run() { + if (myProject.isDisposed()) return; + + if (forceImportAndResolve || myManager.getImportingSettings().isImportAutomatically()) { + myManager.scheduleImportAndResolve().done(new Consumer>() { + @Override + public void consume(List modules) { + promise.setResult(null); + } + }); + } + } + }; } private void onSettingsChange() { diff --git a/plugins/maven/src/main/resources/META-INF/plugin.xml b/plugins/maven/src/main/resources/META-INF/plugin.xml index 09ba3436db51..d3a5c12ff212 100644 --- a/plugins/maven/src/main/resources/META-INF/plugin.xml +++ b/plugins/maven/src/main/resources/META-INF/plugin.xml @@ -180,6 +180,7 @@ + diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/actions/MavenProjectModelModifierTest.java b/plugins/maven/src/test/java/org/jetbrains/idea/maven/actions/MavenProjectModelModifierTest.java new file mode 100644 index 000000000000..9fa14c1b11bd --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/actions/MavenProjectModelModifierTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * 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.jetbrains.idea.maven.actions; + +import com.intellij.openapi.roots.DependencyScope; +import com.intellij.openapi.roots.ExternalLibraryDescriptor; +import com.intellij.openapi.roots.ProjectModelModifier; +import com.intellij.openapi.roots.libraries.Library; +import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiManager; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.concurrency.Promise; +import org.jetbrains.idea.maven.dom.MavenDomWithIndicesTestCase; +import org.jetbrains.idea.maven.importing.MavenProjectModelModifier; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author nik + */ +public class MavenProjectModelModifierTest extends MavenDomWithIndicesTestCase { + public void testAddExternalLibraryDependency() throws IOException { + importProject("test" + + "project" + + "1"); + + Promise result = + getExtension().addExternalLibraryDependency(Collections.singletonList(getModule("project")), new CommonsIoLibraryDescriptor(), + DependencyScope.COMPILE); + assertNotNull(result); + String version = assertHasDependency(myProjectPom, "junit", "junit"); + waitUntilImported(result); + assertModuleLibDep("project", "Maven: junit:junit:" + version); + } + + public void testAddModuleDependency() throws IOException { + createTwoModulesPom("m1", "m2"); + VirtualFile m1 = createModulePom("m1", "test" + + "m1" + + "1"); + createModulePom("m2", "test" + + "m2" + + "1"); + importProject(); + + Promise result = getExtension().addModuleDependency(getModule("m1"), getModule("m2"), DependencyScope.COMPILE); + assertNotNull(result); + assertHasDependency(m1, "test", "m2"); + waitUntilImported(result); + assertModuleModuleDeps("m1", "m2"); + } + + public void testAddLibraryDependency() throws IOException { + createTwoModulesPom("m1", "m2"); + VirtualFile m1 = createModulePom("m1", "test" + + "m1" + + "1"); + createModulePom("m2", "test" + + "m2" + + "1" + + "" + + " " + + " junit" + + " junit" + + " 4.0" + + " test" + + " " + + ""); + importProject(); + + String libName = "Maven: junit:junit:4.0"; + assertModuleLibDep("m2", libName); + Library library = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject).getLibraryByName(libName); + assertNotNull(library); + Promise result = getExtension().addLibraryDependency(getModule("m1"), library, DependencyScope.COMPILE); + assertNotNull(result); + assertHasDependency(m1, "junit", "junit"); + waitUntilImported(result); + assertModuleLibDep("m1", libName); + } + + private void createTwoModulesPom(final String m1, final String m2) throws IOException { + createProjectPom("test" + + "project" + + "pom" + + "1" + + "" + + " " + m1 + "" + + " " + m2 + "" + + ""); + } + + private String assertHasDependency(VirtualFile pom, final String groupId, final String artifactId) { + String pomText = PsiManager.getInstance(myProject).findFile(pom).getText(); + Pattern + pattern = Pattern.compile("(?s).*\\s*" + groupId + "\\s*" + + artifactId + "\\s*(.*)\\s*.*"); + Matcher matcher = pattern.matcher(pomText); + assertTrue(matcher.matches()); + return matcher.group(1); + } + + private void waitUntilImported(Promise result) { + waitForReadingCompletion(); + myProjectsManager.waitForResolvingCompletion(); + myProjectsManager.waitForArtifactsDownloadingCompletion(); + performPostImportTasks(); + myProjectsManager.performScheduledImportInTests(); + assertSame(Promise.State.FULFILLED, result.getState()); + } + + private MavenProjectModelModifier getExtension() { + return ContainerUtil.findInstance(ProjectModelModifier.EP_NAME.getExtensions(myProject), MavenProjectModelModifier.class); + } + + private static class CommonsIoLibraryDescriptor extends ExternalLibraryDescriptor { + public CommonsIoLibraryDescriptor() { + super("junit", "junit"); + } + + @NotNull + @Override + public List getLibraryClassesRoots() { + return Collections.emptyList(); + } + } +}