From ef730e53c820c7875dcb1fbd8f986b59a618aebd Mon Sep 17 00:00:00 2001 From: nik Date: Tue, 21 Apr 2020 18:11:12 +0300 Subject: [PATCH] [platform] make EP used in artifact configuration dynamic (IDEA-238263) GitOrigin-RevId: 46b338834a94feae122d9362a0481b82a1c289d8 --- .../impl/artifacts/ArtifactManagerImpl.java | 73 +++++--- .../impl/artifacts/ArtifactModelImpl.java | 6 +- .../DynamicArtifactExtensionsLoader.kt | 71 +++++++ .../impl/artifacts/InvalidArtifact.java | 3 +- .../DynamicArtifactExtensionsLoaderTest.kt | 173 ++++++++++++++++++ .../ArtifactsStructureConfigurable.java | 19 +- java/java-impl/src/META-INF/JavaPlugin.xml | 6 +- 7 files changed, 314 insertions(+), 37 deletions(-) create mode 100644 java/compiler/impl/src/com/intellij/packaging/impl/artifacts/DynamicArtifactExtensionsLoader.kt create mode 100644 java/compiler/tests/com/intellij/compiler/artifacts/DynamicArtifactExtensionsLoaderTest.kt diff --git a/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/ArtifactManagerImpl.java b/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/ArtifactManagerImpl.java index 2641f9f60b14..effd2f69bedd 100644 --- a/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/ArtifactManagerImpl.java +++ b/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/ArtifactManagerImpl.java @@ -35,6 +35,7 @@ import org.jetbrains.jps.model.serialization.artifact.ArtifactPropertiesState; import org.jetbrains.jps.model.serialization.artifact.ArtifactState; import java.util.*; +import java.util.function.Function; @State(name = ArtifactManagerImpl.COMPONENT_NAME, storages = @Storage(value = "artifacts", stateSplitter = ArtifactManagerStateSplitter.class)) public final class ArtifactManagerImpl extends ArtifactManager implements PersistentStateComponent, Disposable { @@ -55,6 +56,7 @@ public final class ArtifactManagerImpl extends ArtifactManager implements Persis myModel = new ArtifactManagerModel(); myResolvingContext = new DefaultPackagingElementResolvingContext(myProject); ((ArtifactPointerManagerImpl)ArtifactPointerManager.getInstance(project)).setArtifactManager(this); + new DynamicArtifactExtensionsLoader(this).installListeners(this); } @Override @@ -94,36 +96,51 @@ public final class ArtifactManagerImpl extends ArtifactManager implements Persis public ArtifactManagerState getState() { final ArtifactManagerState state = new ArtifactManagerState(); for (Artifact artifact : getAllArtifactsIncludingInvalid()) { - final ArtifactState artifactState; - if (artifact instanceof InvalidArtifact) { - artifactState = ((InvalidArtifact)artifact).getState(); - } - else { - artifactState = new ArtifactState(); - artifactState.setBuildOnMake(artifact.isBuildOnMake()); - artifactState.setName(artifact.getName()); - artifactState.setOutputPath(artifact.getOutputPath()); - artifactState.setRootElement(serializePackagingElement(artifact.getRootElement())); - artifactState.setArtifactType(artifact.getArtifactType().getId()); - ProjectModelExternalSource externalSource = artifact.getExternalSource(); - if (externalSource != null && ProjectUtilCore.isExternalStorageEnabled(myProject)) { - //we can add this attribute only if the artifact configuration will be stored separately, otherwise we will get modified files in .idea/artifacts. - artifactState.setExternalSystemId(externalSource.getId()); - } - - for (ArtifactPropertiesProvider provider : artifact.getPropertiesProviders()) { - final ArtifactPropertiesState propertiesState = serializeProperties(provider, artifact.getProperties(provider)); - if (propertiesState != null) { - artifactState.getPropertiesList().add(propertiesState); - } - } - artifactState.getPropertiesList().sort(Comparator.comparing(ArtifactPropertiesState::getId)); - } - state.getArtifacts().add(artifactState); + state.getArtifacts().add(saveArtifact(artifact)); } return state; } + ArtifactState saveArtifact(Artifact artifact) { + ArtifactState artifactState; + if (artifact instanceof InvalidArtifact) { + artifactState = ((InvalidArtifact)artifact).getState(); + } + else { + artifactState = new ArtifactState(); + artifactState.setBuildOnMake(artifact.isBuildOnMake()); + artifactState.setName(artifact.getName()); + artifactState.setOutputPath(artifact.getOutputPath()); + artifactState.setRootElement(serializePackagingElement(artifact.getRootElement())); + artifactState.setArtifactType(artifact.getArtifactType().getId()); + ProjectModelExternalSource externalSource = artifact.getExternalSource(); + if (externalSource != null && ProjectUtilCore.isExternalStorageEnabled(myProject)) { + //we can add this attribute only if the artifact configuration will be stored separately, otherwise we will get modified files in .idea/artifacts. + artifactState.setExternalSystemId(externalSource.getId()); + } + + for (ArtifactPropertiesProvider provider : artifact.getPropertiesProviders()) { + final ArtifactPropertiesState propertiesState = serializeProperties(provider, artifact.getProperties(provider)); + if (propertiesState != null) { + artifactState.getPropertiesList().add(propertiesState); + } + } + artifactState.getPropertiesList().sort(Comparator.comparing(ArtifactPropertiesState::getId)); + } + return artifactState; + } + + public void replaceArtifacts(@NotNull Collection toReplace, Function replacement) { + if (toReplace.isEmpty()) return; + + ArtifactModelImpl model = createModifiableModel(); + for (Artifact artifact : toReplace) { + model.removeArtifact(artifact); + model.addArtifact(replacement.apply(artifact)); + } + model.commit(); + } + @Nullable private static ArtifactPropertiesState serializeProperties(ArtifactPropertiesProvider provider, ArtifactProperties properties) { final Element options = XmlSerializer.serialize(properties.getState()); @@ -196,7 +213,7 @@ public final class ArtifactManagerImpl extends ArtifactManager implements Persis } } - private ArtifactImpl loadArtifact(ArtifactState state) { + ArtifactImpl loadArtifact(ArtifactState state) { ArtifactType type = ArtifactType.findById(state.getArtifactType()); ProjectModelExternalSource externalSource = findExternalSource(state.getExternalSystemId()); if (type == null) { @@ -299,7 +316,7 @@ public final class ArtifactManagerImpl extends ArtifactManager implements Persis } @Override - public ModifiableArtifactModel createModifiableModel() { + public ArtifactModelImpl createModifiableModel() { return new ArtifactModelImpl(this, getArtifactsList()); } diff --git a/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/ArtifactModelImpl.java b/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/ArtifactModelImpl.java index 12fc37b7c110..14c9b373971f 100644 --- a/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/ArtifactModelImpl.java +++ b/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/ArtifactModelImpl.java @@ -79,13 +79,17 @@ public class ArtifactModelImpl extends ArtifactModelBase implements ModifiableAr final String uniqueName = generateUniqueName(name); final String outputPath = ArtifactUtil.getDefaultArtifactOutputPath(uniqueName, myArtifactManager.getProject()); final ArtifactImpl artifact = new ArtifactImpl(uniqueName, artifactType, false, rootElement, outputPath, externalSource, myDispatcher); + addArtifact(artifact); + return artifact; + } + + void addArtifact(ArtifactImpl artifact) { myOriginalArtifacts.add(artifact); myArtifact2ModifiableCopy.put(artifact, artifact); myModifiable2Original.put(artifact, artifact); artifactsChanged(); myDispatcher.getMulticaster().artifactAdded(artifact); - return artifact; } private String generateUniqueName(String baseName) { diff --git a/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/DynamicArtifactExtensionsLoader.kt b/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/DynamicArtifactExtensionsLoader.kt new file mode 100644 index 000000000000..53aad2ca031a --- /dev/null +++ b/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/DynamicArtifactExtensionsLoader.kt @@ -0,0 +1,71 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.packaging.impl.artifacts + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.extensions.ExtensionPointListener +import com.intellij.openapi.extensions.PluginDescriptor +import com.intellij.packaging.artifacts.Artifact +import com.intellij.packaging.artifacts.ArtifactPropertiesProvider +import com.intellij.packaging.artifacts.ArtifactType +import com.intellij.packaging.elements.PackagingElement +import com.intellij.packaging.elements.PackagingElementType +import com.intellij.util.Processor +import org.jetbrains.annotations.NotNull + +internal class DynamicArtifactExtensionsLoader(private val artifactManager: ArtifactManagerImpl) { + fun installListeners(disposable: Disposable) { + ArtifactType.EP_NAME.getPoint(null).addExtensionPointListener(object : ExtensionPointListener { + override fun extensionAdded(extension: ArtifactType, pluginDescriptor: PluginDescriptor) { + runWriteAction { + reloadArtifacts(artifactManager.allArtifactsIncludingInvalid.filter { + (it as? InvalidArtifact)?.state?.artifactType == extension.id + }) + } + } + + override fun extensionRemoved(extension: ArtifactType, pluginDescriptor: PluginDescriptor) { + reloadArtifacts(artifactManager.getArtifactsByType(extension)) + } + }, false, disposable) + + PackagingElementType.EP_NAME.getPoint(null).addExtensionPointListener(object : ExtensionPointListener>> { + override fun extensionAdded(extension: PackagingElementType>, pluginDescriptor: PluginDescriptor) { + runWriteAction { + reloadArtifacts(artifactManager.allArtifactsIncludingInvalid.filter { + it.artifactType == InvalidArtifactType.getInstance() + }) + } + } + + override fun extensionRemoved(extension: PackagingElementType>, pluginDescriptor: PluginDescriptor) { + reloadArtifacts(artifactManager.artifactsList.filter { containsElementsOfType(it, extension) }) + } + }, false, disposable) + + ArtifactPropertiesProvider.EP_NAME.getPoint(null).addExtensionPointListener(object : ExtensionPointListener { + override fun extensionAdded(extension: ArtifactPropertiesProvider, pluginDescriptor: PluginDescriptor) { + runWriteAction { + reloadArtifacts(artifactManager.allArtifactsIncludingInvalid.filter { artifact -> + (artifact as? InvalidArtifact)?.state?.propertiesList?.any { it.id == extension.id } ?: false + }) + } + } + + override fun extensionRemoved(extension: ArtifactPropertiesProvider, pluginDescriptor: PluginDescriptor) { + reloadArtifacts(artifactManager.artifactsList.filter { extension in it.propertiesProviders }) + } + }, false, disposable) + } + + private fun > containsElementsOfType(artifact: Artifact, type: PackagingElementType): Boolean { + return !ArtifactUtil.processPackagingElements(artifact, type, Processor { false }, + artifactManager.resolvingContext, false) + } + + private fun reloadArtifacts(toReplace: @NotNull Collection) { + artifactManager.replaceArtifacts(toReplace) { + artifactManager.loadArtifact(artifactManager.saveArtifact(it)) + } + } +} \ No newline at end of file diff --git a/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/InvalidArtifact.java b/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/InvalidArtifact.java index e575d3ceeb93..e4eae8ff5400 100644 --- a/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/InvalidArtifact.java +++ b/java/compiler/impl/src/com/intellij/packaging/impl/artifacts/InvalidArtifact.java @@ -17,13 +17,14 @@ package com.intellij.packaging.impl.artifacts; import com.intellij.openapi.roots.ProjectModelExternalSource; import com.intellij.packaging.elements.PackagingElementFactory; +import org.jetbrains.annotations.NotNull; import org.jetbrains.jps.model.serialization.artifact.ArtifactState; public class InvalidArtifact extends ArtifactImpl { private final ArtifactState myState; private final String myErrorMessage; - public InvalidArtifact(ArtifactState state, String errorMessage, ProjectModelExternalSource externalSource) { + public InvalidArtifact(@NotNull ArtifactState state, String errorMessage, ProjectModelExternalSource externalSource) { super(state.getName(), InvalidArtifactType.getInstance(), false, PackagingElementFactory.getInstance().createArtifactRootElement(), "", externalSource); myState = state; diff --git a/java/compiler/tests/com/intellij/compiler/artifacts/DynamicArtifactExtensionsLoaderTest.kt b/java/compiler/tests/com/intellij/compiler/artifacts/DynamicArtifactExtensionsLoaderTest.kt new file mode 100644 index 000000000000..12eaba438106 --- /dev/null +++ b/java/compiler/tests/com/intellij/compiler/artifacts/DynamicArtifactExtensionsLoaderTest.kt @@ -0,0 +1,173 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.compiler.artifacts + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.module.impl.ProjectLoadingErrorsHeadlessNotifier +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.packaging.artifacts.* +import com.intellij.packaging.elements.* +import com.intellij.packaging.impl.artifacts.InvalidArtifactType +import com.intellij.packaging.impl.artifacts.PlainArtifactType +import com.intellij.packaging.ui.ArtifactEditorContext +import com.intellij.packaging.ui.ArtifactPropertiesEditor +import com.intellij.packaging.ui.PackagingElementPresentation +import com.intellij.testFramework.HeavyPlatformTestCase +import com.intellij.util.ui.EmptyIcon +import java.util.function.Consumer +import javax.swing.Icon + +class DynamicArtifactExtensionsLoaderTest : HeavyPlatformTestCase() { + fun `test unload and load artifact type`() { + ProjectLoadingErrorsHeadlessNotifier.setErrorHandler(Consumer {}, testRootDisposable) + val artifactManager = ArtifactManager.getInstance(myProject) + runWithRegisteredExtension(MockArtifactType(), ArtifactType.EP_NAME) { + artifactManager.addArtifact("mock", MockArtifactType.getInstance(), PackagingElementFactory.getInstance().createArtifactRootElement()) + } + + val invalid = assertOneElement(artifactManager.allArtifactsIncludingInvalid) + assertEquals(InvalidArtifactType.getInstance(), invalid.artifactType) + assertEquals("mock", invalid.name) + registerExtension(MockArtifactType(), ArtifactType.EP_NAME, testRootDisposable) + assertOneElement(artifactManager.allArtifactsIncludingInvalid) + val artifact = assertOneElement(artifactManager.getArtifactsByType(MockArtifactType.getInstance())) + assertEquals("mock", artifact.name) + } + + fun `test unload and load packaging element type`() { + val artifactManager = ArtifactManager.getInstance(myProject) + runWithRegisteredExtension(MockPackagingElementType(), PackagingElementType.EP_NAME) { + val root = PackagingElementFactory.getInstance().createArtifactRootElement() + root.addFirstChild(MockPackagingElement().apply { this.state.data = "data" }) + artifactManager.addArtifact("mock", PlainArtifactType.getInstance(), root) + } + + val invalid = assertOneElement(artifactManager.allArtifactsIncludingInvalid) + assertEquals(InvalidArtifactType.getInstance(), invalid.artifactType) + assertEquals("mock", invalid.name) + + registerExtension(MockPackagingElementType(), PackagingElementType.EP_NAME, testRootDisposable) + assertOneElement(artifactManager.allArtifactsIncludingInvalid) + val artifact = assertOneElement(artifactManager.getArtifactsByType(PlainArtifactType.getInstance())) + assertEquals("mock", artifact.name) + assertEquals("data", (artifact.rootElement.children.single() as MockPackagingElement).state.data) + } + + fun `test unload and load artifact properties`() { + val artifactManager = ArtifactManager.getInstance(myProject) + runWithRegisteredExtension(MockArtifactPropertiesProvider(), ArtifactPropertiesProvider.EP_NAME) { + val model = artifactManager.createModifiableModel() + val artifact = model.addArtifact("mock", PlainArtifactType.getInstance()) + artifact.setProperties(MockArtifactPropertiesProvider.getInstance(), MockArtifactProperties().apply { data = "data" }) + runWriteAction { model.commit() } + } + + val invalid = assertOneElement(artifactManager.allArtifactsIncludingInvalid) + assertEquals(InvalidArtifactType.getInstance(), invalid.artifactType) + assertEquals("mock", invalid.name) + + registerExtension(MockArtifactPropertiesProvider(), ArtifactPropertiesProvider.EP_NAME, testRootDisposable) + assertOneElement(artifactManager.allArtifactsIncludingInvalid) + val artifact = assertOneElement(artifactManager.getArtifactsByType(PlainArtifactType.getInstance())) + assertEquals("mock", artifact.name) + assertEquals("data", (artifact.getProperties(MockArtifactPropertiesProvider.getInstance()) as MockArtifactProperties).data) + } + + private inline fun runWithRegisteredExtension(extension: T, extensionPoint: ExtensionPointName, action: () -> Unit) { + val disposable = Disposer.newDisposable() + registerExtension(extension, extensionPoint, disposable) + try { + action() + } + finally { + Disposer.dispose(disposable) + } + } + + private fun registerExtension(type: T, extensionPointName: ExtensionPointName, disposable: Disposable) { + val artifactTypeDisposable = Disposer.newDisposable() + Disposer.register(disposable, Disposable { + runWriteAction { + Disposer.dispose(artifactTypeDisposable) + } + }) + extensionPointName.getPoint(null).registerExtension(type, artifactTypeDisposable) + } + + override fun setUp() { + super.setUp() + ProjectLoadingErrorsHeadlessNotifier.setErrorHandler(Consumer {}, testRootDisposable) + } +} + +private class MockArtifactType : ArtifactType("mock", "Mock") { + companion object { + fun getInstance() = EP_NAME.findExtension(MockArtifactType::class.java)!! + } + + override fun getIcon(): Icon = EmptyIcon.ICON_16 + + override fun getDefaultPathFor(kind: PackagingElementOutputKind): String? = "" + + override fun createRootElement(artifactName: String): CompositePackagingElement<*> { + return PackagingElementFactory.getInstance().createArtifactRootElement() + } +} + +private class MockPackagingElement : PackagingElement(PackagingElementType.EP_NAME.findExtensionOrFail(MockPackagingElementType::class.java)) { + private val state: MockPackagingElementState = MockPackagingElementState("") + + override fun getState(): MockPackagingElementState = state + + override fun loadState(state: MockPackagingElementState) { + this.state.data = state.data + } + + override fun isEqualTo(element: PackagingElement<*>): Boolean = (element as? MockPackagingElement)?.state?.data == state.data + + override fun createPresentation(context: ArtifactEditorContext): PackagingElementPresentation { + throw UnsupportedOperationException() + } +} + +private class MockPackagingElementState(var data: String = "") + +private class MockPackagingElementType : PackagingElementType("mock-element", "Mock Element") { + override fun canCreate(context: ArtifactEditorContext, artifact: Artifact): Boolean = true + + override fun chooseAndCreate(context: ArtifactEditorContext, + artifact: Artifact, + parent: CompositePackagingElement<*>): MutableList> { + throw UnsupportedOperationException() + } + + override fun createEmpty(project: Project): MockPackagingElement { + return MockPackagingElement() + } +} + +private class MockArtifactProperties : ArtifactProperties() { + var data: String = "" + + override fun getState(): MockArtifactProperties? { + return this + } + + override fun loadState(state: MockArtifactProperties) { + data = state.data + } + + override fun createEditor(context: ArtifactEditorContext): ArtifactPropertiesEditor { + throw UnsupportedOperationException() + } +} + +private class MockArtifactPropertiesProvider : ArtifactPropertiesProvider("mock-properties") { + companion object { + fun getInstance(): MockArtifactPropertiesProvider = EP_NAME.findExtensionOrFail(MockArtifactPropertiesProvider::class.java) + } + + override fun createProperties(artifactType: ArtifactType): ArtifactProperties<*> = MockArtifactProperties() +} \ No newline at end of file diff --git a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/ArtifactsStructureConfigurable.java b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/ArtifactsStructureConfigurable.java index 2444d96c676c..cb37322bfe27 100644 --- a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/ArtifactsStructureConfigurable.java +++ b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/ArtifactsStructureConfigurable.java @@ -9,6 +9,8 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.extensions.ExtensionPointListener; +import com.intellij.openapi.extensions.PluginDescriptor; import com.intellij.openapi.module.Module; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.project.DumbAwareAction; @@ -27,7 +29,9 @@ import com.intellij.openapi.ui.NamedConfigurable; import com.intellij.openapi.ui.NonEmptyInputValidator; import com.intellij.openapi.util.Comparing; import com.intellij.packaging.artifacts.*; +import com.intellij.packaging.elements.ComplexPackagingElementType; import com.intellij.packaging.elements.CompositePackagingElement; +import com.intellij.packaging.elements.PackagingElementType; import com.intellij.packaging.impl.artifacts.ArtifactUtil; import com.intellij.packaging.impl.artifacts.InvalidArtifact; import com.intellij.packaging.impl.artifacts.PackagingElementPath; @@ -39,10 +43,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; public class ArtifactsStructureConfigurable extends BaseStructureConfigurable { private ArtifactsStructureConfigurableContextImpl myPackagingEditorContext; @@ -50,6 +51,16 @@ public class ArtifactsStructureConfigurable extends BaseStructureConfigurable { public ArtifactsStructureConfigurable(@NotNull Project project) { super(project, new ArtifactStructureConfigurableState()); + PackagingElementType.EP_NAME.getPoint(null).addExtensionPointListener(new ExtensionPointListener() { + @Override + public void extensionRemoved(@NotNull PackagingElementType extension, @NotNull PluginDescriptor pluginDescriptor) { + if (extension instanceof ComplexPackagingElementType && myDefaultSettings.getTypesToShowContent().contains(extension)) { + List> updated = new ArrayList<>(myDefaultSettings.getTypesToShowContent()); + updated.remove(extension); + myDefaultSettings.setTypesToShowContent(updated); + } + } + }, false, this); } @Override diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index ea640d5b3a91..787238565b34 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -155,11 +155,11 @@ - - + + - +