[project model] keep custom library properties for libraries of unknown type (IDEA-219373)

Such libraries now get special UnknownLibraryType and store the custom configuration as is. If a plugin supporting library type is dynamically loaded, UnknownLibraryType will be replaced by the real type.

GitOrigin-RevId: 3529177996723e29d534e44925fc461f781b0f38
This commit is contained in:
nik
2020-02-05 07:01:46 +00:00
committed by intellij-monorepo-bot
parent 0e648baa59
commit 3f182bfa57
13 changed files with 294 additions and 55 deletions
@@ -5,6 +5,7 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.impl.libraries.LibraryEx
import com.intellij.openapi.roots.impl.libraries.UnknownLibraryKind
import com.intellij.openapi.roots.libraries.*
import com.intellij.openapi.roots.libraries.ui.LibraryEditorComponent
import com.intellij.openapi.roots.libraries.ui.LibraryPropertiesEditor
@@ -16,42 +17,64 @@ import javax.swing.JComponent
class LibraryPropertiesTest : ModuleRootManagerTestCase() {
fun `test set type and properties`() {
val table = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject)
registerLibraryType(testRootDisposable)
addLibrary("custom", "data")
val library = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject).libraries.single() as LibraryEx
assertEquals(MockLibraryType.KIND, library.kind)
assertEquals("data", (library.properties as MockLibraryProperties).data)
assertEquals("""
<library name="custom" type="mock">
<properties>
<option name="data" value="data" />
</properties>
<CLASSES />
<JAVADOC />
<SOURCES />
</library>
""".trimIndent(), LibraryTest.serializeLibraries(myProject))
}
private fun addLibrary(name: String, data: String) {
val table = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject)
runWriteAction {
val model = table.modifiableModel
val lib = model.createLibrary("custom", MockLibraryType.KIND)
val lib = model.createLibrary(name, MockLibraryType.KIND)
val libModel = lib.modifiableModel as LibraryEx.ModifiableModelEx
libModel.properties = MockLibraryProperties("data")
libModel.properties = MockLibraryProperties(data)
libModel.commit()
model.commit()
}
val library = table.libraries.single() as LibraryEx
assertEquals(MockLibraryType.KIND, library.kind)
assertEquals("data", (library.properties as MockLibraryProperties).data)
}
fun `test clear kind when library type is unregistered`() {
fun `test convert to unknown library type is unregistered`() {
val table = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject)
runWithRegisteredType {
addLibrary("lib", "hello")
val library = table.libraries.single() as LibraryEx
assertEquals(MockLibraryType.KIND, library.kind)
assertEquals("hello", (library.properties as MockLibraryProperties).data)
}
val unknown = table.libraries.single() as LibraryEx
assertInstanceOf(unknown.kind, UnknownLibraryKind::class.java)
assertNotNull(unknown.properties)
runWithRegisteredType {
val library = table.libraries.single() as LibraryEx
assertEquals(MockLibraryType.KIND, library.kind)
assertEquals("hello", (library.properties as MockLibraryProperties).data)
}
}
private fun runWithRegisteredType(action: () -> Unit) {
val libraryTypeDisposable = Disposer.newDisposable()
registerLibraryType(libraryTypeDisposable)
try {
runWriteAction {
val model = table.modifiableModel
val lib = model.createLibrary("custom", MockLibraryType.KIND)
model.commit()
}
val library = table.libraries.single() as LibraryEx
assertEquals(MockLibraryType.KIND, library.kind)
assertEquals("default", (library.properties as MockLibraryProperties).data)
action()
}
finally {
Disposer.dispose(libraryTypeDisposable)
}
val library = table.libraries.single() as LibraryEx
assertNull(library.kind)
assertNull(library.properties)
}
private fun registerLibraryType(disposable: Disposable) {
@@ -7,6 +7,7 @@ import com.intellij.java.codeInsight.daemon.quickFix.OrderEntryTest;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.application.ex.PathManagerEx;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.OrderEntryUtil;
import com.intellij.openapi.roots.impl.libraries.LibraryEx;
@@ -89,7 +90,7 @@ public class LibraryTest extends ModuleRootManagerTestCase {
Collections.singletonList(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(localJDomSources)));
assertThat(ModuleRootManagerEx.getInstanceEx(myModule).getModificationCountForTests()).isGreaterThan(moduleModificationCount);
assertThat(serializeLibraries()).isEqualTo(
assertThat(serializeLibraries(myProject)).isEqualTo(
"<library name=\"junit\">\n" +
" <CLASSES>\n" +
" <root url=\"file://$PROJECT_DIR$/jdom-2.0.6.jar\" />\n" +
@@ -281,7 +282,7 @@ public class LibraryTest extends ModuleRootManagerTestCase {
model.addRoot("file://native-lib-root", NativeLibraryOrderRootType.getInstance());
commit(model);
assertThat(serializeLibraries()).isEqualTo(
assertThat(serializeLibraries(myProject)).isEqualTo(
"<library name=\"native\">\n" +
" <CLASSES />\n" +
" <JAVADOC />\n" +
@@ -307,7 +308,7 @@ public class LibraryTest extends ModuleRootManagerTestCase {
model.addJarDirectory("file://jar-dir-src", false, OrderRootType.SOURCES);
commit(model);
assertThat(serializeLibraries()).isEqualTo(
assertThat(serializeLibraries(myProject)).isEqualTo(
"<library name=\"jarDirs\">\n" +
" <CLASSES>\n" +
" <root url=\"file://jar-dir\" />\n" +
@@ -324,12 +325,12 @@ public class LibraryTest extends ModuleRootManagerTestCase {
);
}
private String serializeLibraries() {
StoreUtil.saveSettings(myProject);
static String serializeLibraries(Project project) {
StoreUtil.saveSettings(project);
try {
StringBuilder sb = new StringBuilder();
Element root = JDOMUtil.load(new File(myProject.getProjectFilePath()));
Element root = JDOMUtil.load(new File(project.getProjectFilePath()));
for (Element componentElement : root.getChildren("component")) {
if ("libraryTable".equals(componentElement.getAttributeValue("name"))) {
for (Element libraryElement : componentElement.getChildren("library")) {
@@ -2,6 +2,7 @@
package com.intellij.openapi.roots.impl.libraries;
import com.intellij.ide.ApplicationInitializedListener;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.extensions.ExtensionPointListener;
import com.intellij.openapi.extensions.PluginDescriptor;
import com.intellij.openapi.module.Module;
@@ -13,6 +14,8 @@ import com.intellij.openapi.roots.impl.OrderEntryUtil;
import com.intellij.openapi.roots.libraries.*;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
public class LibraryKindLoader implements ApplicationInitializedListener {
@Override
public void componentsInitialized() {
@@ -21,38 +24,56 @@ public class LibraryKindLoader implements ApplicationInitializedListener {
LibraryType.EP_NAME.getExtensionList();
LibraryType.EP_NAME.addExtensionPointListener(new ExtensionPointListener<LibraryType<?>>() {
@Override
public void extensionAdded(@NotNull LibraryType<?> extension, @NotNull PluginDescriptor pluginDescriptor) {
WriteAction.run(() -> {
LibraryKind.registerKind(extension.getKind());
processAllLibraries(library -> rememberKind(extension.getKind(), library));
});
}
@Override
public void extensionRemoved(@NotNull LibraryType<?> extension, @NotNull PluginDescriptor pluginDescriptor) {
clearKindInAllLibraries(extension.getKind());
LibraryKind.unregisterKind(extension.getKind());
processAllLibraries(library -> forgetKind(extension.getKind(), library));
}
}, null);
}
private static void clearKindInAllLibraries(@NotNull PersistentLibraryKind<?> kind) {
clearKindInLibraries(kind, LibraryTablesRegistrar.getInstance().getLibraryTable());
private static void processAllLibraries(@NotNull Consumer<Library> processor) {
processLibraries(LibraryTablesRegistrar.getInstance().getLibraryTable(), processor);
for (LibraryTable table : LibraryTablesRegistrar.getInstance().getCustomLibraryTables()) {
clearKindInLibraries(kind, table);
processLibraries(table, processor);
}
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
clearKindInLibraries(kind, LibraryTablesRegistrar.getInstance().getLibraryTable(project));
processLibraries(LibraryTablesRegistrar.getInstance().getLibraryTable(project), processor);
for (Module module : ModuleManager.getInstance(project).getModules()) {
for (Library library : OrderEntryUtil.getModuleLibraries(ModuleRootManager.getInstance(module))) {
clearKind(kind, library);
processor.accept(library);
}
}
}
}
private static void clearKindInLibraries(@NotNull PersistentLibraryKind<?> kind, @NotNull LibraryTable table) {
private static void processLibraries(@NotNull LibraryTable table, Consumer<Library> processor) {
for (Library library : table.getLibraries()) {
clearKind(kind, library);
processor.accept(library);
}
}
private static void clearKind(@NotNull PersistentLibraryKind<?> kind, @NotNull Library library) {
private static void forgetKind(@NotNull PersistentLibraryKind<?> kind, @NotNull Library library) {
if (kind.equals(((LibraryEx)library).getKind())) {
LibraryEx.ModifiableModelEx model = (LibraryEx.ModifiableModelEx)library.getModifiableModel();
model.clearKind();
model.forgetKind();
model.commit();
}
}
private static void rememberKind(@NotNull PersistentLibraryKind<?> kind, @NotNull Library library) {
PersistentLibraryKind<?> libraryKind = ((LibraryEx)library).getKind();
if (libraryKind instanceof UnknownLibraryKind && libraryKind.getKindId().equals(kind.getKindId())) {
LibraryEx.ModifiableModelEx model = (LibraryEx.ModifiableModelEx)library.getModifiableModel();
model.restoreKind();
model.commit();
}
}
@@ -19,6 +19,7 @@ import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.impl.libraries.UnknownLibraryKind;
import com.intellij.openapi.roots.libraries.ui.LibraryEditorComponent;
import com.intellij.openapi.roots.libraries.ui.LibraryPropertiesEditor;
import com.intellij.openapi.roots.libraries.ui.LibraryRootsComponentDescriptor;
@@ -108,6 +109,9 @@ public abstract class LibraryType<P extends LibraryProperties> extends LibraryPr
return type;
}
}
if (kind instanceof UnknownLibraryKind) {
return new UnknownLibraryType((UnknownLibraryKind)kind);
}
throw new IllegalArgumentException("Library with kind " + kind + " is not registered");
}
}
@@ -0,0 +1,41 @@
// 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.openapi.roots.libraries;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.impl.libraries.UnknownLibraryKind;
import com.intellij.openapi.roots.libraries.ui.LibraryEditorComponent;
import com.intellij.openapi.roots.libraries.ui.LibraryPropertiesEditor;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
public class UnknownLibraryType extends LibraryType<UnknownLibraryKind.UnknownLibraryProperties> {
public UnknownLibraryType(@NotNull UnknownLibraryKind kind) {
super(kind);
}
@Override
public @Nullable String getCreateActionName() {
return null;
}
@Override
public @Nullable NewLibraryConfiguration createNewLibrary(@NotNull JComponent parentComponent,
@Nullable VirtualFile contextDirectory,
@NotNull Project project) {
return null;
}
@Override
public @Nullable LibraryPropertiesEditor createPropertiesEditor(@NotNull LibraryEditorComponent<UnknownLibraryKind.UnknownLibraryProperties> editorComponent) {
return null;
}
@Override
public @Nullable Icon getIcon(UnknownLibraryKind.@Nullable UnknownLibraryProperties properties) {
return AllIcons.Nodes.Unknown;
}
}
@@ -82,7 +82,9 @@ public interface LibraryEx extends Library {
/**
* Removes custom library kind and associated properties if any
*/
void clearKind();
void forgetKind();
void restoreKind();
/**
* Add a URL to list of directories excluded from the library. The directory specified by {@code url} must be located under some
@@ -15,6 +15,7 @@
*/
package com.intellij.openapi.roots.libraries;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
@@ -30,7 +31,8 @@ public class LibraryKind {
*/
public LibraryKind(@NotNull @NonNls String kindId) {
myKindId = kindId;
if (ourAllKinds.containsKey(kindId)) {
LibraryKind kind = ourAllKinds.get(kindId);
if (kind != null && !(kind instanceof TemporaryLibraryKind)) {
throw new IllegalArgumentException("Kind " + kindId + " is not unique");
}
ourAllKinds.put(kindId, this);
@@ -56,4 +58,14 @@ public class LibraryKind {
public static LibraryKind findById(String kindId) {
return ourAllKinds.get(kindId);
}
@ApiStatus.Internal
public static void unregisterKind(@NotNull LibraryKind kind) {
ourAllKinds.remove(kind.getKindId());
}
@ApiStatus.Internal
public static void registerKind(@NotNull LibraryKind kind) {
ourAllKinds.put(kind.getKindId(), kind);
}
}
@@ -0,0 +1,11 @@
// 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.openapi.roots.libraries;
import org.jetbrains.annotations.ApiStatus;
/**
* Marker interface for implementations of {@link LibraryKind} which are used to replace the real implementations. For internal use only.
*/
@ApiStatus.Internal
public interface TemporaryLibraryKind {
}
@@ -117,8 +117,12 @@ public class LibraryImpl extends TraceableDisposable implements LibraryEx.Modifi
@Nullable
private static PersistentLibraryKind<?> findPersistentLibraryKind(@NotNull Element element) {
String typeString = element.getAttributeValue(LIBRARY_TYPE_ATTR);
if (typeString == null) return null;
LibraryKind kind = LibraryKind.findById(typeString);
if (kind != null && !(kind instanceof PersistentLibraryKind<?>)) {
if (kind == null) {
return UnknownLibraryKind.getOrCreate(typeString);
}
if (!(kind instanceof PersistentLibraryKind<?>)) {
LOG.error("Cannot load non-persistable library kind: " + typeString);
return null;
}
@@ -293,10 +297,16 @@ public class LibraryImpl extends TraceableDisposable implements LibraryEx.Modifi
if (typeId == null) return;
myKind = (PersistentLibraryKind<?>) LibraryKind.findById(typeId);
if (myKind == null) return;
final Element propertiesElement = element.getChild(JpsLibraryTableSerializer.PROPERTIES_TAG);
if (myKind == null) {
myKind = UnknownLibraryKind.getOrCreate(typeId);
UnknownLibraryKind.UnknownLibraryProperties properties = new UnknownLibraryKind.UnknownLibraryProperties();
properties.setConfiguration(propertiesElement);
myProperties = properties;
return;
}
myProperties = myKind.createDefaultProperties();
final Element propertiesElement = element.getChild(JpsLibraryTableSerializer.PROPERTIES_TAG);
if (propertiesElement != null) {
ComponentSerializationUtil.loadComponentState(myProperties, propertiesElement);
}
@@ -354,7 +364,7 @@ public class LibraryImpl extends TraceableDisposable implements LibraryEx.Modifi
LOG.assertTrue(myProperties != null, "Properties is 'null' in library with kind " + myKind);
final Object state = myProperties.getState();
if (state != null) {
final Element propertiesElement = XmlSerializer.serialize(state);
final Element propertiesElement = state instanceof Element ? ((Element)state).clone() : XmlSerializer.serialize(state);
if (propertiesElement != null) {
element.addContent(propertiesElement.setName(JpsLibraryTableSerializer.PROPERTIES_TAG));
}
@@ -438,9 +448,30 @@ public class LibraryImpl extends TraceableDisposable implements LibraryEx.Modifi
}
@Override
public void clearKind() {
myKind = null;
myProperties = null;
public void forgetKind() {
if (myKind == null) return;
myKind = UnknownLibraryKind.getOrCreate(myKind.getKindId());
Object propertiesState = myProperties.getState();
if (propertiesState != null) {
UnknownLibraryKind.UnknownLibraryProperties properties = new UnknownLibraryKind.UnknownLibraryProperties();
properties.setConfiguration(XmlSerializer.serialize(propertiesState));
myProperties = properties;
}
else {
myProperties = null;
}
}
@Override
public void restoreKind() {
if (myKind == null || !(myKind instanceof UnknownLibraryKind)) return;
myKind = (PersistentLibraryKind<?>)LibraryKind.findById(myKind.getKindId());
Element configuration = ((UnknownLibraryKind.UnknownLibraryProperties)myProperties).getConfiguration();
myProperties = myKind.createDefaultProperties();
if (configuration != null) {
ComponentSerializationUtil.loadComponentState(myProperties, configuration);
}
}
@Override
@@ -0,0 +1,89 @@
// 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.openapi.roots.impl.libraries;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.roots.libraries.LibraryKind;
import com.intellij.openapi.roots.libraries.LibraryProperties;
import com.intellij.openapi.roots.libraries.PersistentLibraryKind;
import com.intellij.openapi.roots.libraries.TemporaryLibraryKind;
import com.intellij.openapi.util.JDOMUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class UnknownLibraryKind extends PersistentLibraryKind<UnknownLibraryKind.UnknownLibraryProperties> implements TemporaryLibraryKind {
private static final Logger LOG = Logger.getInstance(UnknownLibraryKind.class);
private UnknownLibraryKind(@NotNull String kindId) {
super(kindId);
}
@Override
public int hashCode() {
return getKindId().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj instanceof UnknownLibraryKind && ((UnknownLibraryKind)obj).getKindId().equals(getKindId());
}
@NotNull
@Override
public UnknownLibraryProperties createDefaultProperties() {
return new UnknownLibraryProperties();
}
public static UnknownLibraryKind getOrCreate(@NotNull String kindId) {
LibraryKind kind = LibraryKind.findById(kindId);
if (kind instanceof UnknownLibraryKind) {
return (UnknownLibraryKind)kind;
}
if (kind != null) {
LOG.error("Trying to create UnknownLibraryKind for known kind " + kind);
}
return new UnknownLibraryKind(kindId);
}
@Override
public String toString() {
return "UnknownLibraryKind:" + getKindId();
}
public static class UnknownLibraryProperties extends LibraryProperties<Element> {
private Element myConfiguration;
public Element getConfiguration() {
return myConfiguration;
}
public void setConfiguration(Element configuration) {
myConfiguration = configuration;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UnknownLibraryProperties)) return false;
Element configuration = ((UnknownLibraryProperties)obj).myConfiguration;
if (configuration == null && myConfiguration == null) return true;
if (configuration == null || myConfiguration == null) return false;
return JDOMUtil.areElementsEqual(myConfiguration, configuration);
}
@Override
public int hashCode() {
return JDOMUtil.hashCode(myConfiguration, false);
}
@Nullable
@Override
public Element getState() {
return myConfiguration;
}
@Override
public void loadState(@NotNull Element state) {
myConfiguration = state;
}
}
}
@@ -117,7 +117,7 @@ internal class LegacyBridgeLibraryImpl(
private val libraryEntity
get() = entityStore.cachedValue(libraryEntityValue, entityId)
private val snapshotValue = CachedValueWithParameter { storage, id: LibraryId ->
internal val snapshotValue = CachedValueWithParameter { storage, id: LibraryId ->
LibraryViaTypedEntity(
libraryImpl = this,
libraryEntity = storage.resolve(id) ?: object : LibraryEntity {
@@ -28,6 +28,7 @@ internal class LegacyBridgeLibraryModifiableModelImpl(
) : LegacyBridgeModifiableBase(diff), LibraryEx.ModifiableModelEx, LibraryEx, RootProvider {
private var entityId = originalLibrarySnapshot.libraryEntity.persistentId()
private var reloadKind = false
private val currentLibraryValue = CachedValue { storage ->
val newLibrary = LibraryViaTypedEntity(
@@ -79,9 +80,13 @@ internal class LegacyBridgeLibraryModifiableModelImpl(
assertModelIsLive()
modelIsCommittedOrDisposed = true
if (!isChanged) return
committer(this, diff)
if (reloadKind) {
originalLibrary.entityStore.clearCachedValue(originalLibrary.snapshotValue, originalLibrary.entityId)
}
if (isChanged) {
committer(this, diff)
}
}
private fun update(updater: ModifiableLibraryEntity.() -> Unit) {
@@ -274,14 +279,12 @@ internal class LegacyBridgeLibraryModifiableModelImpl(
}
}
override fun clearKind() {
assertModelIsLive()
if (kind == null) return
override fun forgetKind() {
reloadKind = true
}
val properties = currentLibrary.libraryEntity.referrers(LibraryPropertiesEntity::library).toList()
for (propertiesEntity in properties) {
diff.removeEntity(propertiesEntity)
}
override fun restoreKind() {
reloadKind = true
}
private fun isUnderRoots(url: VirtualFileUrl, roots: Collection<LibraryRoot>): Boolean {
@@ -7,6 +7,7 @@ import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.roots.ProjectModelExternalSource
import com.intellij.openapi.roots.RootProvider
import com.intellij.openapi.roots.impl.libraries.LibraryEx
import com.intellij.openapi.roots.impl.libraries.UnknownLibraryKind
import com.intellij.openapi.roots.libraries.*
import com.intellij.openapi.util.JDOMUtil
import com.intellij.openapi.vfs.VirtualFile
@@ -36,7 +37,7 @@ internal class LibraryViaTypedEntity(val libraryImpl: LegacyBridgeLibraryImpl,
LegacyBridgeFileContainer(urls, jarDirs)
}
private val excludedRoots = if (libraryEntity.excludedRoots.isNotEmpty()) LegacyBridgeFileContainer(libraryEntity.excludedRoots, emptyList()) else null
private val libraryKind = libraryEntity.getCustomProperties()?.libraryType?.let { LibraryKind.findById(it) } as? PersistentLibraryKind<*>
private val libraryKind = libraryEntity.getCustomProperties()?.libraryType?.let { LibraryKind.findById(it) ?: UnknownLibraryKind.getOrCreate(it) } as? PersistentLibraryKind<*>
private val properties = loadProperties()
private fun loadProperties(): LibraryProperties<*>? {