diff options
author | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-08-28 14:13:29 +0200 |
---|---|---|
committer | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-08-29 17:48:13 +0200 |
commit | e19ef5983707e6a5c8d127f1ac8f02754cef82fd (patch) | |
tree | 9e3852cb9abc81ed6aa444465928d45fd7763dea /platform/extensions |
New upstream version 0~183.5153.4+dfsg
Diffstat (limited to 'platform/extensions')
51 files changed, 4803 insertions, 0 deletions
diff --git a/platform/extensions/intellij.platform.extensions.iml b/platform/extensions/intellij.platform.extensions.iml new file mode 100644 index 00000000..246056a0 --- /dev/null +++ b/platform/extensions/intellij.platform.extensions.iml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module relativePaths="true" type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" name="JDOM" level="project" /> + <orderEntry type="library" name="picocontainer" level="project" /> + <orderEntry type="module" module-name="intellij.platform.util" /> + <orderEntry type="library" scope="TEST" name="JUnit4" level="project" /> + <orderEntry type="library" scope="TEST" name="Xerces" level="project" /> + <orderEntry type="library" scope="TEST" name="assertJ" level="project" /> + </component> + <component name="copyright"> + <Base> + <setting name="state" value="0" /> + </Base> + <LanguageOptions name="HTML"> + <option name="templateOptions"> + <value> + <option name="block" value="true" /> + <option name="separateBefore" value="false" /> + <option name="separateAfter" value="false" /> + <option name="prefixLines" value="true" /> + <option name="lenBefore" value="80" /> + <option name="lenAfter" value="80" /> + <option name="box" value="false" /> + <option name="filler" value=" " /> + </value> + </option> + <option name="notice" value="Copyright (c) &#36;today.year, Your Corporation. All Rights Reserved." /> + <option name="keyword" value="Copyright" /> + <option name="fileTypeOverride" value="2" /> + <option name="relativeBefore" value="true" /> + <option name="addBlankAfter" value="true" /> + <option name="fileLocation" value="1" /> + <option name="useAlternate" value="false" /> + </LanguageOptions> + <LanguageOptions name="JAVA"> + <option name="templateOptions"> + <value> + <option name="block" value="true" /> + <option name="separateBefore" value="false" /> + <option name="separateAfter" value="false" /> + <option name="prefixLines" value="true" /> + <option name="lenBefore" value="80" /> + <option name="lenAfter" value="80" /> + <option name="box" value="false" /> + <option name="filler" value=" " /> + </value> + </option> + <option name="notice" value="/* * Copyright 2000-2005 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. */" /> + <option name="keyword" value="Copyright" /> + <option name="fileTypeOverride" value="2" /> + <option name="relativeBefore" value="true" /> + <option name="addBlankAfter" value="true" /> + <option name="fileLocation" value="1" /> + <option name="useAlternate" value="false" /> + </LanguageOptions> + <LanguageOptions name="JSP"> + <option name="templateOptions"> + <value> + <option name="block" value="true" /> + <option name="separateBefore" value="false" /> + <option name="separateAfter" value="false" /> + <option name="prefixLines" value="true" /> + <option name="lenBefore" value="80" /> + <option name="lenAfter" value="80" /> + <option name="box" value="false" /> + <option name="filler" value=" " /> + </value> + </option> + <option name="notice" value="Copyright (c) &#36;today.year, Your Corporation. All Rights Reserved." /> + <option name="keyword" value="Copyright" /> + <option name="fileTypeOverride" value="2" /> + <option name="relativeBefore" value="true" /> + <option name="addBlankAfter" value="true" /> + <option name="fileLocation" value="1" /> + <option name="useAlternate" value="false" /> + </LanguageOptions> + <LanguageOptions name="JavaScript"> + <option name="templateOptions"> + <value> + <option name="block" value="true" /> + <option name="separateBefore" value="false" /> + <option name="separateAfter" value="false" /> + <option name="prefixLines" value="true" /> + <option name="lenBefore" value="80" /> + <option name="lenAfter" value="80" /> + <option name="box" value="false" /> + <option name="filler" value=" " /> + </value> + </option> + <option name="notice" value="Copyright (c) &#36;today.year, Your Corporation. All Rights Reserved." /> + <option name="keyword" value="Copyright" /> + <option name="fileTypeOverride" value="2" /> + <option name="relativeBefore" value="true" /> + <option name="addBlankAfter" value="true" /> + <option name="fileLocation" value="1" /> + <option name="useAlternate" value="false" /> + </LanguageOptions> + <LanguageOptions name="Properties"> + <option name="templateOptions"> + <value> + <option name="block" value="true" /> + <option name="separateBefore" value="false" /> + <option name="separateAfter" value="false" /> + <option name="prefixLines" value="true" /> + <option name="lenBefore" value="80" /> + <option name="lenAfter" value="80" /> + <option name="box" value="false" /> + <option name="filler" value=" " /> + </value> + </option> + <option name="notice" value="Copyright (c) &#36;today.year, Your Corporation. All Rights Reserved." /> + <option name="keyword" value="Copyright" /> + <option name="fileTypeOverride" value="2" /> + <option name="relativeBefore" value="true" /> + <option name="addBlankAfter" value="true" /> + <option name="fileLocation" value="1" /> + <option name="useAlternate" value="false" /> + </LanguageOptions> + <LanguageOptions name="XML"> + <option name="templateOptions"> + <value> + <option name="block" value="true" /> + <option name="separateBefore" value="false" /> + <option name="separateAfter" value="false" /> + <option name="prefixLines" value="true" /> + <option name="lenBefore" value="80" /> + <option name="lenAfter" value="80" /> + <option name="box" value="false" /> + <option name="filler" value=" " /> + </value> + </option> + <option name="notice" value="Copyright (c) &#36;today.year, Your Corporation. All Rights Reserved." /> + <option name="keyword" value="Copyright" /> + <option name="fileTypeOverride" value="2" /> + <option name="relativeBefore" value="true" /> + <option name="addBlankAfter" value="true" /> + <option name="fileLocation" value="1" /> + <option name="useAlternate" value="false" /> + </LanguageOptions> + <LanguageOptions name="__TEMPLATE__"> + <option name="templateOptions"> + <value> + <option name="block" value="true" /> + <option name="separateBefore" value="false" /> + <option name="separateAfter" value="false" /> + <option name="prefixLines" value="true" /> + <option name="lenBefore" value="80" /> + <option name="lenAfter" value="80" /> + <option name="box" value="false" /> + <option name="filler" value=" " /> + </value> + </option> + <option name="notice" value="Copyright 2000-&#36;{today.year} 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." /> + <option name="keyword" value="Copyright" /> + <option name="fileTypeOverride" value="4" /> + <option name="relativeBefore" value="true" /> + <option name="addBlankAfter" value="true" /> + <option name="fileLocation" value="1" /> + <option name="useAlternate" value="false" /> + </LanguageOptions> + </component> +</module>
\ No newline at end of file diff --git a/platform/extensions/src/com/intellij/openapi/extensions/AbstractExtensionPointBean.java b/platform/extensions/src/com/intellij/openapi/extensions/AbstractExtensionPointBean.java new file mode 100644 index 00000000..2511b241 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/AbstractExtensionPointBean.java @@ -0,0 +1,71 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.pico.CachingConstructorInjectionComponentAdapter; +import com.intellij.util.xmlb.annotations.Transient; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.picocontainer.PicoContainer; + +/** + * @author peter + */ +public abstract class AbstractExtensionPointBean implements PluginAware { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.extensions.AbstractExtensionPointBean"); + protected PluginDescriptor myPluginDescriptor; + + @Transient + public PluginDescriptor getPluginDescriptor() { + return myPluginDescriptor; + } + + @Override + public final void setPluginDescriptor(PluginDescriptor pluginDescriptor) { + myPluginDescriptor = pluginDescriptor; + } + + @Nullable + public PluginId getPluginId() { + return myPluginDescriptor == null ? null : myPluginDescriptor.getPluginId(); + } + + @NotNull + public final <T> Class<T> findClass(@NotNull String className) throws ClassNotFoundException { + return (Class<T>)Class.forName(className, true, getLoaderForClass()); + } + + @Nullable + public final <T> Class<T> findClassNoExceptions(final String className) { + try { + return findClass(className); + } + catch (ClassNotFoundException e) { + LOG.error("Problem loading class " + className + " from plugin " + myPluginDescriptor.getPluginId().getIdString(), e); + return null; + } + } + + @NotNull + public ClassLoader getLoaderForClass() { + return myPluginDescriptor == null ? getClass().getClassLoader() : myPluginDescriptor.getPluginClassLoader(); + } + + @NotNull + public final <T> T instantiate(final String className, @NotNull final PicoContainer container) throws ClassNotFoundException { + return instantiate(this.findClass(className), container); + } + + @NotNull + public static <T> T instantiate(@NotNull final Class<T> aClass, @NotNull final PicoContainer container) { + return instantiate(aClass, container, false); + } + + @NotNull + public static <T> T instantiate(@NotNull final Class<T> aClass, + @NotNull final PicoContainer container, + final boolean allowNonPublicClasses) { + return (T)new CachingConstructorInjectionComponentAdapter(aClass.getName(), aClass, null, allowNonPublicClasses).getComponentInstance(container); + } + +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/AreaInstance.java b/platform/extensions/src/com/intellij/openapi/extensions/AreaInstance.java new file mode 100644 index 00000000..c0f8b09e --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/AreaInstance.java @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +/** + * @author akireyev + */ +public interface AreaInstance { +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/AreaListener.java b/platform/extensions/src/com/intellij/openapi/extensions/AreaListener.java new file mode 100644 index 00000000..a2386369 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/AreaListener.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +import org.jetbrains.annotations.NotNull; + +/** + * @author akireyev + */ +public interface AreaListener { + void areaCreated(@NotNull String areaClass, @NotNull AreaInstance areaInstance); + void areaDisposing(@NotNull String areaClass, @NotNull AreaInstance areaInstance); +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/AreaPicoContainer.java b/platform/extensions/src/com/intellij/openapi/extensions/AreaPicoContainer.java new file mode 100644 index 00000000..be60bf58 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/AreaPicoContainer.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +import org.picocontainer.MutablePicoContainer; + +/** + * @author Alexander Kireyev + */ +public interface AreaPicoContainer extends MutablePicoContainer { +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/BaseExtensionPointName.java b/platform/extensions/src/com/intellij/openapi/extensions/BaseExtensionPointName.java new file mode 100644 index 00000000..d48c08be --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/BaseExtensionPointName.java @@ -0,0 +1,22 @@ +// Copyright 2000-2018 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.extensions; + +import org.jetbrains.annotations.NotNull; + +public abstract class BaseExtensionPointName { + private final String myName; + + public BaseExtensionPointName(@NotNull String name) { + myName = name; + } + + @NotNull + public final String getName() { + return myName; + } + + @Override + public final String toString() { + return myName; + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/CustomLoadingExtensionPointBean.java b/platform/extensions/src/com/intellij/openapi/extensions/CustomLoadingExtensionPointBean.java new file mode 100644 index 00000000..e1354407 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/CustomLoadingExtensionPointBean.java @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.extensions; + +import com.intellij.util.xmlb.annotations.Attribute; +import org.jetbrains.annotations.NotNull; +import org.picocontainer.PicoContainer; + +/** + * @author yole + */ +public class CustomLoadingExtensionPointBean extends AbstractExtensionPointBean { + @Attribute("factoryClass") + public String factoryClass; + + @Attribute("factoryArgument") + public String factoryArgument; + + @NotNull + protected Object instantiateExtension(String implementationClass, @NotNull PicoContainer picoContainer) throws ClassNotFoundException { + if (factoryClass != null) { + ExtensionFactory factory = instantiate(factoryClass, picoContainer); + return factory.createInstance(factoryArgument, implementationClass); + } + else { + if (implementationClass == null) { + throw new RuntimeException("implementation class is not specified for unknown language extension point, " + + "plugin id: " + + (myPluginDescriptor == null ? "<not available>" : myPluginDescriptor.getPluginId()) + ". " + + "Check if 'implementationClass' attribute is specified"); + } + return instantiate(findClass(implementationClass), picoContainer, true); + } + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/DefaultPluginDescriptor.java b/platform/extensions/src/com/intellij/openapi/extensions/DefaultPluginDescriptor.java new file mode 100644 index 00000000..e3a0e364 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/DefaultPluginDescriptor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +import org.jetbrains.annotations.NotNull; + +/** + * @author Alexander Kireyev + */ +public class DefaultPluginDescriptor implements PluginDescriptor { + @NotNull + private final PluginId myPluginId; + private final ClassLoader myPluginClassLoader; + + public DefaultPluginDescriptor(String pluginId) { + myPluginId = PluginId.getId(pluginId); + myPluginClassLoader = null; + } + + public DefaultPluginDescriptor(@NotNull PluginId pluginId) { + myPluginId = pluginId; + myPluginClassLoader = null; + } + + public DefaultPluginDescriptor(@NotNull PluginId pluginId, final ClassLoader pluginClassLoader) { + myPluginId = pluginId; + myPluginClassLoader = pluginClassLoader; + } + + @Override + @NotNull + public PluginId getPluginId() { + return myPluginId; + } + + @Override + public ClassLoader getPluginClassLoader() { + return myPluginClassLoader; + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/EPAvailabilityListenerExtension.java b/platform/extensions/src/com/intellij/openapi/extensions/EPAvailabilityListenerExtension.java new file mode 100644 index 00000000..21a75c9d --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/EPAvailabilityListenerExtension.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +import org.jetbrains.annotations.NotNull; + +/** + * @author AKireyev + */ +public class EPAvailabilityListenerExtension implements PluginAware { + public static final String EXTENSION_POINT_NAME = "com.intellij.openapi.extensions.epAvailabilityListener"; + + private String myExtensionPointName; + private String myListenerClass; + private PluginDescriptor myPluginDescriptor; + + public EPAvailabilityListenerExtension() { + } + + public EPAvailabilityListenerExtension(@NotNull String extensionPointName, @NotNull String listenerClass) { + myExtensionPointName = extensionPointName; + myListenerClass = listenerClass; + } + + @NotNull + public String getExtensionPointName() { + return myExtensionPointName; + } + + public void setExtensionPointName(@NotNull String extensionPointName) { + myExtensionPointName = extensionPointName; + } + + @NotNull + public String getListenerClass() { + return myListenerClass; + } + + public void setListenerClass(@NotNull String listenerClass) { + myListenerClass = listenerClass; + } + + @Override + public void setPluginDescriptor(PluginDescriptor pluginDescriptor) { + myPluginDescriptor = pluginDescriptor; + } + + public PluginDescriptor getPluginDescriptor() { + return myPluginDescriptor; + } + + public Class loadListenerClass() throws ClassNotFoundException { + if (myPluginDescriptor != null && myPluginDescriptor.getPluginClassLoader() != null) { + return Class.forName(getListenerClass(), true, myPluginDescriptor.getPluginClassLoader()); + } + else { + return Class.forName(getListenerClass()); + } + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/Extension.java b/platform/extensions/src/com/intellij/openapi/extensions/Extension.java new file mode 100644 index 00000000..0f6a62c8 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/Extension.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +import org.jetbrains.annotations.NotNull; + +/** + * @author kir + * + * An extension can implement this interface to get notifications when it is added/removed to {@link ExtensionPoint} + */ +public interface Extension { + void extensionAdded(@NotNull ExtensionPoint extensionPoint); + void extensionRemoved(@NotNull ExtensionPoint extensionPoint); +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ExtensionException.java b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionException.java new file mode 100644 index 00000000..90eeeae2 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2017 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 com.intellij.openapi.extensions; + +import org.jetbrains.annotations.NotNull; + +public class ExtensionException extends RuntimeException{ + private final Class<?> myExtensionClass; + + public ExtensionException(@NotNull Class<?> extensionClass) { + super(extensionClass.getCanonicalName()); + + myExtensionClass = extensionClass; + } + + public ExtensionException(@NotNull Class<?> extensionClass, @NotNull Throwable cause) { + super(extensionClass.getCanonicalName(), cause); + + myExtensionClass = extensionClass; + } + + @NotNull + public Class<?> getExtensionClass() { + return myExtensionClass; + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ExtensionFactory.java b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionFactory.java new file mode 100644 index 00000000..8256e6ed --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionFactory.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +/** + * @author yole + */ +public interface ExtensionFactory { + Object createInstance(final String factoryArgument, String implementationClass); +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPoint.java b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPoint.java new file mode 100644 index 00000000..09305bf7 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPoint.java @@ -0,0 +1,65 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.openapi.Disposable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Stream; + +public interface ExtensionPoint<T> { + @NotNull + String getName(); + + AreaInstance getArea(); + + void registerExtension(@NotNull T extension); + + void registerExtension(@NotNull T extension, @NotNull LoadingOrder order); + + /** + * Prefer to use {@link #getExtensionList()}. + */ + @NotNull + T[] getExtensions(); + + @NotNull + List<T> getExtensionList(); + + @NotNull + Stream<T> extensions(); + + boolean hasAnyExtensions(); + + @Nullable + T getExtension(); + + boolean hasExtension(@NotNull T extension); + + void unregisterExtension(@NotNull T extension); + + /** + * Unregisters an extension of the specified type. + */ + void unregisterExtension(@NotNull Class<? extends T> extensionClass); + + void addExtensionPointListener(@NotNull ExtensionPointListener<T> listener, @NotNull Disposable parentDisposable); + + void addExtensionPointListener(@NotNull ExtensionPointListener<T> listener); + + void removeExtensionPointListener(@NotNull ExtensionPointListener<T> extensionPointListener); + + void reset(); + + @NotNull + Class<T> getExtensionClass(); + + @NotNull + Kind getKind(); + + @NotNull + String getClassName(); + + enum Kind {INTERFACE, BEAN_CLASS} +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointAndAreaListener.java b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointAndAreaListener.java new file mode 100644 index 00000000..277fa4d9 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointAndAreaListener.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2009 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. + */ + +/* + * @author max + */ +package com.intellij.openapi.extensions; + +import org.jetbrains.annotations.NotNull; + +public interface ExtensionPointAndAreaListener<T> extends ExtensionPointListener<T> { + void areaReplaced(@NotNull ExtensionsArea oldArea); +}
\ No newline at end of file diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointAvailabilityListener.java b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointAvailabilityListener.java new file mode 100644 index 00000000..d2926c54 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointAvailabilityListener.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +import org.jetbrains.annotations.NotNull; + +/** + * @author AKireyev + */ +public interface ExtensionPointAvailabilityListener { + void extensionPointRegistered(@NotNull ExtensionPoint extensionPoint); + void extensionPointRemoved(@NotNull ExtensionPoint extensionPoint); +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointListener.java b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointListener.java new file mode 100644 index 00000000..e1c5ae8a --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointListener.java @@ -0,0 +1,15 @@ +// Copyright 2000-2018 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.extensions; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface ExtensionPointListener<T> { + ExtensionPointListener[] EMPTY_ARRAY = new ExtensionPointListener[0]; + + default void extensionAdded(@NotNull T extension, @Nullable PluginDescriptor pluginDescriptor) { + } + + default void extensionRemoved(@NotNull T extension, @Nullable PluginDescriptor pluginDescriptor) { + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointName.java b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointName.java new file mode 100644 index 00000000..1a000d91 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionPointName.java @@ -0,0 +1,86 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Stream; + +public final class ExtensionPointName<T> extends BaseExtensionPointName { + public ExtensionPointName(@NotNull String name) { + super(name); + } + + @NotNull + public static <T> ExtensionPointName<T> create(@NotNull @NonNls final String name) { + return new ExtensionPointName<>(name); + } + + /** + * Prefer to use {@link #getExtensionList()}. + */ + @NotNull + public T[] getExtensions() { + return getPoint(null).getExtensions(); + } + + @NotNull + public List<T> getExtensionList() { + return getPoint(null).getExtensionList(); + } + + @NotNull + public Stream<T> extensions() { + return getPoint(null).extensions(); + } + + public boolean hasAnyExtensions() { + return getPoint(null).hasAnyExtensions(); + } + + /** + * Consider using {@link ProjectExtensionPointName#getExtensions(AreaInstance)} + */ + @NotNull + public List<T> getExtensionList(@Nullable AreaInstance areaInstance) { + return getPoint(areaInstance).getExtensionList(); + } + + /** + * Consider using {@link ProjectExtensionPointName#getExtensions(AreaInstance)} + */ + @NotNull + public T[] getExtensions(@Nullable AreaInstance areaInstance) { + return getPoint(areaInstance).getExtensions(); + } + + /** + * Consider using {@link ProjectExtensionPointName#extensions(AreaInstance)} + */ + @NotNull + public Stream<T> extensions(@Nullable AreaInstance areaInstance) { + return getPoint(areaInstance).extensions(); + } + + @NotNull + public ExtensionPoint<T> getPoint(@Nullable AreaInstance areaInstance) { + return Extensions.getArea(areaInstance).getExtensionPoint(getName()); + } + + @Nullable + public <V extends T> V findExtension(@NotNull Class<V> instanceOf) { + return ContainerUtil.findInstance(getExtensionList(), instanceOf); + } + + @NotNull + public <V extends T> V findExtensionOrFail(@NotNull Class<V> instanceOf) { + V result = findExtension(instanceOf); + if (result == null) { + throw new IllegalArgumentException("could not find extension implementation " + instanceOf); + } + return result; + } +}
\ No newline at end of file diff --git a/platform/extensions/src/com/intellij/openapi/extensions/Extensions.java b/platform/extensions/src/com/intellij/openapi/extensions/Extensions.java new file mode 100644 index 00000000..d7770132 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/Extensions.java @@ -0,0 +1,227 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.extensions.impl.ExtensionsAreaImpl; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.SystemInfoRt; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class Extensions { + public static final ExtensionPointName<AreaListener> AREA_LISTENER_EXTENSION_POINT = new ExtensionPointName<>("com.intellij.arealistener"); + private static final Map<AreaInstance, ExtensionsAreaImpl> ourAreaInstance2area = ContainerUtil.newConcurrentMap(); + private static final Map<String, AreaClassConfiguration> ourAreaClass2Configuration = ContainerUtil.newConcurrentMap(); + + @NotNull + private static ExtensionsAreaImpl ourRootArea = createRootArea(); + + private Extensions() { + } + + @NotNull + private static ExtensionsAreaImpl createRootArea() { + ExtensionsAreaImpl rootArea = new ExtensionsAreaImpl(null, null, null); + rootArea.registerExtensionPoint(AREA_LISTENER_EXTENSION_POINT.getName(), AreaListener.class.getName()); + return rootArea; + } + + /** + * @return instance containing application-level extensions + */ + @NotNull + public static ExtensionsArea getRootArea() { + return ourRootArea; + } + + /** + * If {@code areaInstance} is a project returns instance containing project-level extensions for that project + * if {@code areaInstance} is a module returns instance containing module-level extensions for that module, + * if {@code areaInstance} is {@code null} returns instance containing application-level extensions. + */ + @NotNull + public static ExtensionsArea getArea(@Nullable("null means root") AreaInstance areaInstance) { + if (areaInstance == null) { + return ourRootArea; + } + ExtensionsAreaImpl area = ourAreaInstance2area.get(areaInstance); + if (area == null) { + throw new IllegalArgumentException("No area instantiated for: " + areaInstance); + } + return area; + } + + @TestOnly + public static void cleanRootArea(@NotNull Disposable parentDisposable) { + final ExtensionsAreaImpl oldRootArea = (ExtensionsAreaImpl)getRootArea(); + final ExtensionsAreaImpl newArea = createRootArea(); + ourRootArea = newArea; + oldRootArea.notifyAreaReplaced(newArea); + Disposer.register(parentDisposable, () -> { + ourRootArea = oldRootArea; + newArea.notifyAreaReplaced(oldRootArea); + }); + } + + @NotNull + public static Object[] getExtensions(@NonNls @NotNull String extensionPointName) { + return getExtensions(extensionPointName, null); + } + + /** + * @deprecated Use {@link ExtensionPointName#getExtensionList()} + */ + @Deprecated + @NotNull + public static <T> T[] getExtensions(@NotNull ExtensionPointName<T> extensionPointName) { + return extensionPointName.getExtensions(); + } + + /** + * @deprecated Use {@link ProjectExtensionPointName#getExtensions(AreaInstance)} + */ + @Deprecated + @NotNull + public static <T> T[] getExtensions(@NotNull ExtensionPointName<T> extensionPointName, @Nullable AreaInstance areaInstance) { + return extensionPointName.getExtensions(areaInstance); + } + + @NotNull + public static <T> T[] getExtensions(@NotNull String extensionPointName, @Nullable("null means root") AreaInstance areaInstance) { + ExtensionsArea area = getArea(areaInstance); + ExtensionPoint<T> extensionPoint = area.getExtensionPoint(extensionPointName); + return extensionPoint.getExtensions(); + } + + /** + * @deprecated Use {@link ExtensionPointName#findExtensionOrFail(Class)} + */ + @Deprecated + @NotNull + public static <T, U extends T> U findExtension(@NotNull ExtensionPointName<T> extensionPointName, @NotNull Class<U> extClass) { + return extensionPointName.findExtensionOrFail(extClass); + } + + @NotNull + public static <T, U extends T> U findExtension(@NotNull ExtensionPointName<T> extensionPointName, AreaInstance areaInstance, @NotNull Class<U> extClass) { + for (T t : extensionPointName.getExtensions(areaInstance)) { + if (extClass.isInstance(t)) { + //noinspection unchecked + return (U) t; + } + } + throw new IllegalArgumentException("could not find extension implementation " + extClass); + } + + public static void instantiateArea(@NonNls @NotNull String areaClass, @NotNull AreaInstance areaInstance, @Nullable("null means root") AreaInstance parentAreaInstance) { + AreaClassConfiguration configuration = ourAreaClass2Configuration.get(areaClass); + if (configuration == null) { + throw new IllegalArgumentException("Area class is not registered: " + areaClass); + } + ExtensionsArea parentArea = getArea(parentAreaInstance); + if (!Objects.equals(parentArea.getAreaClass(), configuration.getParentClassName())) { + throw new IllegalArgumentException("Wrong parent area. Expected class: " + configuration.getParentClassName() + " actual class: " + parentArea.getAreaClass()); + } + ExtensionsAreaImpl area = new ExtensionsAreaImpl(areaClass, areaInstance, parentArea.getPicoContainer()); + if (ourAreaInstance2area.put(areaInstance, area) != null) { + throw new IllegalArgumentException("Area already instantiated for: " + areaInstance); + } + for (AreaListener listener : getAreaListeners()) { + listener.areaCreated(areaClass, areaInstance); + } + } + + @NotNull + private static List<AreaListener> getAreaListeners() { + return getRootArea().getExtensionPoint(AREA_LISTENER_EXTENSION_POINT).getExtensionList(); + } + + public static void registerAreaClass(@NonNls @NotNull String areaClass, @Nullable @NonNls String parentAreaClass) { + if (ourAreaClass2Configuration.containsKey(areaClass)) { + // allow duplicate area class registrations if they are the same - fixing duplicate registration in tests is much more trouble + AreaClassConfiguration configuration = ourAreaClass2Configuration.get(areaClass); + if (!Objects.equals(configuration.getParentClassName(), parentAreaClass)) { + throw new RuntimeException("Area class already registered: " + areaClass + ", "+ configuration); + } + else { + return; + } + } + AreaClassConfiguration configuration = new AreaClassConfiguration(areaClass, parentAreaClass); + ourAreaClass2Configuration.put(areaClass, configuration); + } + + public static void disposeArea(@NotNull AreaInstance areaInstance) { + assert ourAreaInstance2area.containsKey(areaInstance); + + String areaClass = ourAreaInstance2area.get(areaInstance).getAreaClass(); + if (areaClass == null) { + throw new IllegalArgumentException("Area class is null (area never instantiated?). Instance: " + areaInstance); + } + try { + for (AreaListener listener : getAreaListeners()) { + listener.areaDisposing(areaClass, areaInstance); + } + } + finally { + ourAreaInstance2area.remove(areaInstance); + } + } + + private static class AreaClassConfiguration { + private final String myClassName; + private final String myParentClassName; + + private AreaClassConfiguration(@NotNull String className, String parentClassName) { + myClassName = className; + myParentClassName = parentClassName; + } + + @NotNull + public String getClassName() { + return myClassName; + } + + public String getParentClassName() { + return myParentClassName; + } + + @Override + public String toString() { + return "AreaClassConfiguration{myClassName='" + myClassName + '\'' + ", myParentClassName='" + myParentClassName + "'}"; + } + } + + public static boolean isComponentSuitableForOs(@Nullable String os) { + if (StringUtil.isEmpty(os)) { + return true; + } + + if (os.equals("mac")) { + return SystemInfoRt.isMac; + } + else if (os.equals("linux")) { + return SystemInfoRt.isLinux; + } + else if (os.equals("windows")) { + return SystemInfoRt.isWindows; + } + else if (os.equals("unix")) { + return SystemInfoRt.isUnix; + } + else if (os.equals("freebsd")) { + return SystemInfoRt.isFreeBSD; + } + else { + throw new IllegalArgumentException("Unknown OS " + os); + } + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ExtensionsArea.java b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionsArea.java new file mode 100644 index 00000000..af9fe61c --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ExtensionsArea.java @@ -0,0 +1,50 @@ +// Copyright 2000-2018 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.extensions; + +import org.jdom.Element; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @see Extensions#getArea(AreaInstance) + * @see Extensions#getRootArea() + */ +public interface ExtensionsArea { + void registerExtensionPoint(@NonNls @NotNull String extensionPointName, @NotNull String extensionPointBeanClass); + void registerExtensionPoint(@NonNls @NotNull String extensionPointName, @NotNull String extensionPointBeanClass, @NotNull ExtensionPoint.Kind kind); + void unregisterExtensionPoint(@NonNls @NotNull String extensionPointName); + + boolean hasExtensionPoint(@NonNls @NotNull String extensionPointName); + + boolean hasExtensionPoint(@NotNull ExtensionPointName<?> extensionPointName); + + @NotNull + <T> ExtensionPoint<T> getExtensionPoint(@NonNls @NotNull String extensionPointName); + + @NotNull + <T> ExtensionPoint<T> getExtensionPoint(@NotNull ExtensionPointName<T> extensionPointName); + + @NotNull + ExtensionPoint[] getExtensionPoints(); + + void addAvailabilityListener(@NotNull String extensionPointName, @NotNull ExtensionPointAvailabilityListener listener); + void removeAvailabilityListener(@NotNull String extensionPointName, @NotNull ExtensionPointAvailabilityListener listener); + + @NotNull + AreaPicoContainer getPicoContainer(); + + void registerExtensionPoint(@NotNull PluginDescriptor pluginDescriptor, @NotNull Element extensionPointElement); + + /** + * Registers a new extension. + * @param pluginDescriptor plugin to which extension belongs + * @param extensionElement element from plugin.xml file where extension settings are specified + * @param extensionNs extension namespace which is prepended to the tag name from {@code extensionElement} to form the qualified extension name. + */ + void registerExtension(@NotNull PluginDescriptor pluginDescriptor, @NotNull Element extensionElement, @Nullable String extensionNs); + + void registerExtension(@NotNull final ExtensionPoint extensionPoint, @NotNull final PluginDescriptor pluginDescriptor, @NotNull final Element extensionElement); + + String getAreaClass(); +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/KeyedFactoryEPBean.java b/platform/extensions/src/com/intellij/openapi/extensions/KeyedFactoryEPBean.java new file mode 100644 index 00000000..64926534 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/KeyedFactoryEPBean.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +import com.intellij.util.xmlb.annotations.Attribute; + +/** + * @author yole + */ +public class KeyedFactoryEPBean extends AbstractExtensionPointBean { + // these must be public for scrambling compatibility + @Attribute("key") + public String key; + + @Attribute("implementationClass") + public String implementationClass; + + @Attribute("factoryClass") + public String factoryClass; +}
\ No newline at end of file diff --git a/platform/extensions/src/com/intellij/openapi/extensions/LoadingOrder.java b/platform/extensions/src/com/intellij/openapi/extensions/LoadingOrder.java new file mode 100644 index 00000000..0ce7349b --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/LoadingOrder.java @@ -0,0 +1,208 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.openapi.util.Couple; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.graph.CachingSemiGraph; +import com.intellij.util.graph.DFSTBuilder; +import com.intellij.util.graph.GraphGenerator; +import com.intellij.util.graph.InboundSemiGraph; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * All extensions can have an "order" attribute in their XML element that will affect the place where this extension will appear in the + * {@link ExtensionPoint#getExtensions()}. Possible values are "first", "last", "before ID" and "after ID" where ID + * is another same-type extension ID. Values can be combined in a comma-separated way. E.g. if you wish to plug before some extension XXX + * that has "first" as its order, you must be "first, before XXX". The same with "last".<p> + * + * Extension ID can be specified in the "id" attribute of corresponding XML element. When you specify order, it's usually a good practice + * to specify also id, to allow other plugin-writers to plug relatively to your extension.<p> + * + * If some anchor id can't be resolved, the constraint is ignored. + * + * @author Alexander Kireyev + */ +public class LoadingOrder { + @NonNls public static final String FIRST_STR = "first"; + @NonNls public static final String LAST_STR = "last"; + @NonNls public static final String BEFORE_STR = "before "; + @NonNls public static final String BEFORE_STR_OLD = "before:"; + @NonNls public static final String AFTER_STR = "after "; + @NonNls public static final String AFTER_STR_OLD = "after:"; + + @NonNls public static final String ORDER_RULE_SEPARATOR = ","; + + public static final LoadingOrder ANY = new LoadingOrder(); + public static final LoadingOrder FIRST = new LoadingOrder(FIRST_STR); + public static final LoadingOrder LAST = new LoadingOrder(LAST_STR); + + @NonNls private final String myName; // for debug only + private final boolean myFirst; + private final boolean myLast; + private final Set<String> myBefore = new LinkedHashSet<>(2); + private final Set<String> myAfter = new LinkedHashSet<>(2); + + private LoadingOrder() { + myName = "ANY"; + myFirst = false; + myLast = false; + } + + private LoadingOrder(@NonNls @NotNull String text) { + myName = text; + boolean last = false; + boolean first = false; + for (final String string : StringUtil.split(text, ORDER_RULE_SEPARATOR)) { + String trimmed = string.trim(); + if (trimmed.equalsIgnoreCase(FIRST_STR)) first = true; + else if (trimmed.equalsIgnoreCase(LAST_STR)) last = true; + else if (StringUtil.startsWithIgnoreCase(trimmed, BEFORE_STR)) myBefore.add(trimmed.substring(BEFORE_STR.length()).trim()); + else if (StringUtil.startsWithIgnoreCase(trimmed, BEFORE_STR_OLD)) myBefore.add(trimmed.substring(BEFORE_STR_OLD.length()).trim()); + else if (StringUtil.startsWithIgnoreCase(trimmed, AFTER_STR)) myAfter.add(trimmed.substring(AFTER_STR.length()).trim()); + else if (StringUtil.startsWithIgnoreCase(trimmed, AFTER_STR_OLD)) myAfter.add(trimmed.substring(AFTER_STR_OLD.length()).trim()); + else throw new AssertionError("Invalid specification: " + trimmed + "; should be one of FIRST, LAST, BEFORE <id> or AFTER <id>"); + } + myFirst = first; + myLast = last; + } + + public String toString() { + return myName; + } + + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof LoadingOrder)) return false; + + final LoadingOrder that = (LoadingOrder)o; + + if (myFirst != that.myFirst) return false; + if (myLast != that.myLast) return false; + if (!myAfter.equals(that.myAfter)) return false; + if (!myBefore.equals(that.myBefore)) return false; + + return true; + } + + public int hashCode() { + int result = myFirst ? 1 : 0; + result = 31 * result + (myLast ? 1 : 0); + result = 31 * result + myBefore.hashCode(); + result = 31 * result + myAfter.hashCode(); + return result; + } + + public static LoadingOrder before(@NonNls final String id) { + return new LoadingOrder(BEFORE_STR + id); + } + + public static LoadingOrder after(@NonNls final String id) { + return new LoadingOrder(AFTER_STR + id); + } + + public static void sort(@NotNull Orderable... orderable) { + sort(Arrays.asList(orderable)); + } + + public static void sort(@NotNull final List<? extends Orderable> orderable) { + // our graph is pretty sparse so do benefit from the fact + final Map<String, Orderable> map = ContainerUtil.newLinkedHashMap(); + final Map<Orderable, LoadingOrder> cachedMap = ContainerUtil.newLinkedHashMap(); + final Set<Orderable> first = new LinkedHashSet<>(1); + final Set<Orderable> hasBefore = new LinkedHashSet<>(orderable.size()); + for (Orderable o : orderable) { + String id = o.getOrderId(); + if (StringUtil.isNotEmpty(id)) map.put(id, o); + LoadingOrder order = o.getOrder(); + cachedMap.put(o, order); + if (order.myFirst) first.add(o); + if (!order.myBefore.isEmpty()) hasBefore.add(o); + } + + InboundSemiGraph<Orderable> graph = new InboundSemiGraph<Orderable>() { + @NotNull + @Override + public Collection<Orderable> getNodes() { + List<Orderable> list = ContainerUtil.newArrayList(orderable); + Collections.reverse(list); + return list; + } + + @NotNull + @Override + public Iterator<Orderable> getIn(Orderable n) { + LoadingOrder order = cachedMap.get(n); + + Set<Orderable> predecessors = new LinkedHashSet<>(); + for (String id : order.myAfter) { + Orderable o = map.get(id); + if (o != null) { + predecessors.add(o); + } + } + + String id = n.getOrderId(); + if (StringUtil.isNotEmpty(id)) { + for (Orderable o : hasBefore) { + LoadingOrder hisOrder = cachedMap.get(o); + if (hisOrder.myBefore.contains(id)) { + predecessors.add(o); + } + } + } + + if (order.myLast) { + for (Orderable o : orderable) { + LoadingOrder hisOrder = cachedMap.get(o); + if (!hisOrder.myLast) { + predecessors.add(o); + } + } + } + + if (!order.myFirst) { + predecessors.addAll(first); + } + + return predecessors.iterator(); + } + }; + + DFSTBuilder<Orderable> builder = new DFSTBuilder<>(GraphGenerator.generate(CachingSemiGraph.cache(graph))); + + if (!builder.isAcyclic()) { + Couple<Orderable> p = builder.getCircularDependency(); + throw new SortingException("Could not satisfy sorting requirements", p.first, p.second); + } + + orderable.sort(builder.comparator()); + } + + @NotNull + public static LoadingOrder readOrder(@Nullable String orderAttr) { + if (orderAttr == null) { + return ANY; + } + else if (orderAttr.equals(FIRST_STR)) { + return FIRST; + } + else if (orderAttr.equals(LAST_STR)) { + return LAST; + } + else { + return new LoadingOrder(orderAttr); + } + } + + public interface Orderable { + @Nullable + String getOrderId(); + + LoadingOrder getOrder(); + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/PluginAware.java b/platform/extensions/src/com/intellij/openapi/extensions/PluginAware.java new file mode 100644 index 00000000..6fa42ce7 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/PluginAware.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +/** + * Extensions should implement this interface when it is important to find out what particular plugin has provided this extension. + * @author akireyev + */ +public interface PluginAware { + /** + * Called by extensions framework when extension is loaded from plugin.xml descriptor. + * @param pluginDescriptor descriptor of the plugin that provided this particular extension. + */ + void setPluginDescriptor(PluginDescriptor pluginDescriptor); +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/PluginDescriptor.java b/platform/extensions/src/com/intellij/openapi/extensions/PluginDescriptor.java new file mode 100644 index 00000000..601d8fb0 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/PluginDescriptor.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions; + +/** + * @author Alexander Kireyev + */ +public interface PluginDescriptor { + PluginId getPluginId(); + ClassLoader getPluginClassLoader(); +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/PluginId.java b/platform/extensions/src/com/intellij/openapi/extensions/PluginId.java new file mode 100644 index 00000000..b83036b7 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/PluginId.java @@ -0,0 +1,75 @@ +/* + * 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 com.intellij.openapi.extensions; + +import gnu.trove.THashMap; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +/** + * @author max + */ +public class PluginId implements Comparable<PluginId> { + public static final PluginId[] EMPTY_ARRAY = new PluginId[0]; + + private static final Map<String, PluginId> ourRegisteredIds = new THashMap<>(); + + private final String myIdString; + + private PluginId(@NotNull String idString) { + myIdString = idString; + } + + @Override + public int compareTo(@NotNull PluginId o) { + return myIdString.compareTo(o.myIdString); + } + + @NotNull + public static synchronized PluginId getId(@NotNull String idString) { + return ourRegisteredIds.computeIfAbsent(idString, PluginId::new); + } + + @Nullable + public static synchronized PluginId findId(@NotNull String... idStrings) { + for (String idString : idStrings) { + PluginId pluginId = ourRegisteredIds.get(idString); + if (pluginId != null) { + return pluginId; + } + } + return null; + } + + @NonNls + @NotNull + public String getIdString() { + return myIdString; + } + + @Override + public String toString() { + return getIdString(); + } + + @NotNull + public static synchronized Map<String, PluginId> getRegisteredIds() { + return new THashMap<>(ourRegisteredIds); + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/ProjectExtensionPointName.java b/platform/extensions/src/com/intellij/openapi/extensions/ProjectExtensionPointName.java new file mode 100644 index 00000000..399c3c82 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/ProjectExtensionPointName.java @@ -0,0 +1,35 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Stream; + +public final class ProjectExtensionPointName<T> extends BaseExtensionPointName { + public ProjectExtensionPointName(@NotNull String name) { + super(name); + } + + @NotNull + public ExtensionPoint<T> getPoint(@NotNull AreaInstance areaInstance) { + return Extensions.getArea(areaInstance).getExtensionPoint(getName()); + } + + @NotNull + public List<T> getExtensions(@NotNull AreaInstance areaInstance) { + return getPoint(areaInstance).getExtensionList(); + } + + @NotNull + public Stream<T> extensions(@NotNull AreaInstance areaInstance) { + return getPoint(areaInstance).extensions(); + } + + @Nullable + public <V extends T> V findExtension(@NotNull Class<V> instanceOf, @NotNull AreaInstance areaInstance) { + return ContainerUtil.findInstance(getExtensions(areaInstance), instanceOf); + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/SimpleSmartExtensionPoint.java b/platform/extensions/src/com/intellij/openapi/extensions/SimpleSmartExtensionPoint.java new file mode 100644 index 00000000..a6e9f43c --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/SimpleSmartExtensionPoint.java @@ -0,0 +1,26 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.util.SmartList; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * @author peter + */ +public abstract class SimpleSmartExtensionPoint<T> extends SmartExtensionPoint<T,T>{ + public SimpleSmartExtensionPoint(@NotNull final Collection<T> explicitExtensions) { + super(explicitExtensions); + } + + public SimpleSmartExtensionPoint() { + super(new SmartList<>()); + } + + @Override + @NotNull + protected final T getExtension(@NotNull final T t) { + return t; + } +}
\ No newline at end of file diff --git a/platform/extensions/src/com/intellij/openapi/extensions/SmartExtensionPoint.java b/platform/extensions/src/com/intellij/openapi/extensions/SmartExtensionPoint.java new file mode 100644 index 00000000..a466b69a --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/SmartExtensionPoint.java @@ -0,0 +1,87 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author peter + */ +public abstract class SmartExtensionPoint<Extension, V> implements ExtensionPointAndAreaListener<Extension> { + private final Collection<V> myExplicitExtensions; + private ExtensionPoint<Extension> myExtensionPoint; + private List<V> myCache; + + protected SmartExtensionPoint(@NotNull final Collection<V> explicitExtensions) { + myExplicitExtensions = explicitExtensions; + } + + @NotNull + protected abstract ExtensionPoint<Extension> getExtensionPoint(); + + public final void addExplicitExtension(@NotNull V extension) { + synchronized (myExplicitExtensions) { + myExplicitExtensions.add(extension); + myCache = null; + } + } + + public final void removeExplicitExtension(@NotNull V extension) { + synchronized (myExplicitExtensions) { + myExplicitExtensions.remove(extension); + myCache = null; + } + } + + @Nullable + protected abstract V getExtension(@NotNull final Extension extension); + + @NotNull + public final List<V> getExtensions() { + synchronized (myExplicitExtensions) { + List<V> result = myCache; + if (result == null) { + myExtensionPoint = getExtensionPoint(); + // EP will not add duplicated listener, so, it is safe to not care about is already added + myExtensionPoint.addExtensionPointListener(this); + + List<V> registeredExtensions = ContainerUtil.mapNotNull(myExtensionPoint.getExtensions(), this::getExtension); + result = new ArrayList<>(myExplicitExtensions.size() + registeredExtensions.size()); + result.addAll(myExplicitExtensions); + result.addAll(registeredExtensions); + myCache = result; + } + return result; + } + } + + @Override + public final void extensionAdded(@NotNull final Extension extension, @Nullable final PluginDescriptor pluginDescriptor) { + dropCache(); + } + + public final void dropCache() { + synchronized (myExplicitExtensions) { + if (myCache != null) { + myCache = null; + myExtensionPoint.removeExtensionPointListener(this); + myExtensionPoint = null; + } + } + } + + @Override + public final void extensionRemoved(@NotNull final Extension extension, @Nullable final PluginDescriptor pluginDescriptor) { + dropCache(); + } + + @Override + public final void areaReplaced(@NotNull final ExtensionsArea area) { + dropCache(); + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/SortingException.java b/platform/extensions/src/com/intellij/openapi/extensions/SortingException.java new file mode 100644 index 00000000..548ce14e --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/SortingException.java @@ -0,0 +1,23 @@ +// Copyright 2000-2018 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.extensions; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; + +/** + * @author Alexander Kireyev + */ +public class SortingException extends RuntimeException { + private final LoadingOrder.Orderable[] myConflictingElements; + + SortingException(String message, @NotNull LoadingOrder.Orderable... conflictingElements) { + super(message + ": " + StringUtil.join(conflictingElements, + item -> item.getOrderId() + "(" + item.getOrder() + ")", "; ")); + myConflictingElements = conflictingElements; + } + + @NotNull + public LoadingOrder.Orderable[] getConflictingElements() { + return myConflictingElements; + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionComponentAdapter.java b/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionComponentAdapter.java new file mode 100644 index 00000000..7d231b18 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionComponentAdapter.java @@ -0,0 +1,173 @@ +// Copyright 2000-2018 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.extensions.impl; + +import com.intellij.openapi.extensions.LoadingOrder; +import com.intellij.openapi.extensions.PluginAware; +import com.intellij.openapi.extensions.PluginDescriptor; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.util.pico.AssignableToComponentAdapter; +import com.intellij.util.pico.CachingConstructorInjectionComponentAdapter; +import com.intellij.util.xmlb.XmlSerializer; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.picocontainer.*; + +/** + * @author Alexander Kireyev + */ +public class ExtensionComponentAdapter implements LoadingOrder.Orderable, AssignableToComponentAdapter { + public static final ExtensionComponentAdapter[] EMPTY_ARRAY = new ExtensionComponentAdapter[0]; + + private Object myComponentInstance; + @Nullable + private final Element myExtensionElement; + private final PicoContainer myContainer; + private final PluginDescriptor myPluginDescriptor; + @NotNull + private Object myImplementationClassOrName; // Class or String + private boolean myNotificationSent; + + private final String myOrderId; + private final LoadingOrder myOrder; + + public ExtensionComponentAdapter(@NotNull String implementationClassName, + @Nullable PicoContainer container, + @Nullable PluginDescriptor pluginDescriptor, + @Nullable String orderId, + @NotNull LoadingOrder order, + @Nullable Element extensionElement) { + myImplementationClassOrName = implementationClassName; + myContainer = container; + myPluginDescriptor = pluginDescriptor; + myExtensionElement = extensionElement; + + myOrderId = orderId; + myOrder = order; + } + + public ExtensionComponentAdapter(@NotNull String implementationClassName, + @Nullable Element extensionElement, + PicoContainer container, + PluginDescriptor pluginDescriptor) { + this(implementationClassName, container, pluginDescriptor, null, LoadingOrder.ANY, extensionElement); + } + + @Override + public Object getComponentKey() { + return this; + } + + @Override + public Class getComponentImplementation() { + return loadImplementationClass(); + } + + @Override + public Object getComponentInstance(final PicoContainer container) throws PicoException, ProcessCanceledException { + if (myComponentInstance == null) { + try { + Class impl = loadImplementationClass(); + Object componentInstance = new CachingConstructorInjectionComponentAdapter(getComponentKey(), impl, null, true).getComponentInstance(container); + + if (myExtensionElement != null) { + try { + XmlSerializer.deserializeInto(componentInstance, myExtensionElement); + } + catch (Exception e) { + throw new PicoInitializationException(e); + } + } + + myComponentInstance = componentInstance; + } + catch (ProcessCanceledException e) { + throw e; + } + catch (Throwable t) { + PluginId pluginId = myPluginDescriptor != null ? myPluginDescriptor.getPluginId() : null; + throw new PicoPluginExtensionInitializationException(t.getMessage(), t, pluginId); + } + + if (myComponentInstance instanceof PluginAware) { + PluginAware pluginAware = (PluginAware)myComponentInstance; + pluginAware.setPluginDescriptor(myPluginDescriptor); + } + } + + return myComponentInstance; + } + + @Override + public void verify(PicoContainer container) throws PicoIntrospectionException { + throw new UnsupportedOperationException("Method verify is not supported in " + getClass()); + } + + @Override + public void accept(PicoVisitor visitor) { + throw new UnsupportedOperationException("Method accept is not supported in " + getClass()); + } + + public Object getExtension() { + return getComponentInstance(myContainer); + } + + @Override + public LoadingOrder getOrder() { + return myOrder; + } + + @Override + public final String getOrderId() { + return myOrderId; + } + + public PluginId getPluginName() { + return myPluginDescriptor.getPluginId(); + } + + public PluginDescriptor getPluginDescriptor() { + return myPluginDescriptor; + } + + @NotNull + private Class loadImplementationClass() { + Object implementationClassOrName = myImplementationClassOrName; + if (implementationClassOrName instanceof String) { + try { + ClassLoader classLoader = myPluginDescriptor == null ? getClass().getClassLoader() : myPluginDescriptor.getPluginClassLoader(); + if (classLoader == null) { + classLoader = getClass().getClassLoader(); + } + myImplementationClassOrName = implementationClassOrName = Class.forName((String)implementationClassOrName, false, classLoader); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + return (Class)implementationClassOrName; + } + + @Override + public String getAssignableToClassName() { + Object implementationClassOrName = myImplementationClassOrName; + if (implementationClassOrName instanceof String) { + return (String)implementationClassOrName; + } + return ((Class)implementationClassOrName).getName(); + } + + boolean isNotificationSent() { + return myNotificationSent; + } + + void setNotificationSent(boolean notificationSent) { + myNotificationSent = notificationSent; + } + + @Override + public String toString() { + return "ExtensionComponentAdapter[" + getAssignableToClassName() + "]: plugin=" + myPluginDescriptor; + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionPointImpl.java b/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionPointImpl.java new file mode 100644 index 00000000..2abe8974 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionPointImpl.java @@ -0,0 +1,546 @@ +// Copyright 2000-2018 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.extensions.impl; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.*; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.Disposer; +import com.intellij.util.ArrayUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.StringInterner; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.stream.Stream; + +/** + * @author AKireyev + */ +@SuppressWarnings("SynchronizeOnThis") +public final class ExtensionPointImpl<T> implements ExtensionPoint<T> { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.extensions.impl.ExtensionPointImpl"); + + private final AreaInstance myArea; + private final String myName; + private final String myClassName; + private final Kind myKind; + + private volatile List<T> myExtensionsCache; + // Since JDK 9 Arrays.ArrayList.toArray() doesn't return T[] array (https://bugs.openjdk.java.net/browse/JDK-6260652), + // but instead returns Object[], so, we cannot use toArray() anymore. + // Only array.clone should be used because of performance reasons (https://youtrack.jetbrains.com/issue/IDEA-198172). + private volatile T[] myExtensionsCacheAsArray; + + private final ExtensionsAreaImpl myOwner; + private final PluginDescriptor myDescriptor; + + @NotNull + private Set<ExtensionComponentAdapter> myExtensionAdapters = Collections.emptySet(); // guarded by this + @SuppressWarnings("unchecked") + @NotNull + private ExtensionPointListener<T>[] myEPListeners = ExtensionPointListener.EMPTY_ARRAY; // guarded by this + @NotNull + private List<ExtensionComponentAdapter> myLoadedAdapters = Collections.emptyList(); // guarded by this + + private Class<T> myExtensionClass; + + private static final StringInterner INTERNER = new StringInterner(); + + ExtensionPointImpl(@NotNull String name, + @NotNull String className, + @NotNull Kind kind, + @NotNull ExtensionsAreaImpl owner, + AreaInstance area, + @NotNull PluginDescriptor descriptor) { + synchronized (INTERNER) { + myName = INTERNER.intern(name); + } + myClassName = className; + myKind = kind; + myOwner = owner; + myArea = area; + myDescriptor = descriptor; + } + + @NotNull + @Override + public String getName() { + return myName; + } + + @Override + public AreaInstance getArea() { + return myArea; + } + + @NotNull + @Override + public String getClassName() { + return myClassName; + } + + @NotNull + @Override + public Kind getKind() { + return myKind; + } + + @Override + public void registerExtension(@NotNull T extension) { + registerExtension(extension, LoadingOrder.ANY); + } + + @NotNull + public PluginDescriptor getDescriptor() { + return myDescriptor; + } + + @Override + public synchronized void registerExtension(@NotNull T extension, @NotNull LoadingOrder order) { + ExtensionComponentAdapter adapter = new ObjectComponentAdapter(extension, order); + + if (LoadingOrder.ANY == order) { + int index = myLoadedAdapters.size(); + while (index > 0) { + ExtensionComponentAdapter lastAdapter = myLoadedAdapters.get(index - 1); + if (lastAdapter.getOrder() == LoadingOrder.LAST) { + index--; + } + else { + break; + } + } + if (getExtensionIndex(extension) != -1) { + LOG.error("Extension was already added: " + extension); + return; + } + + registerExtension(extension, adapter, index, true); + } + else { + registerExtensionAdapter(adapter); + processAdapters(); + } + } + + private void registerExtension(@NotNull T extension, @NotNull ExtensionComponentAdapter adapter, int index, boolean runNotifications) { + Class<T> extensionClass = getExtensionClass(); + if (!extensionClass.isInstance(extension)) { + LOG.error("Extension " + extension.getClass() + " does not implement " + extensionClass); + return; + } + if (myLoadedAdapters == Collections.<ExtensionComponentAdapter>emptyList()) myLoadedAdapters = new ArrayList<>(); + myLoadedAdapters.add(index, adapter); + + if (runNotifications) { + clearCache(); + + if (!adapter.isNotificationSent()) { + if (extension instanceof Extension) { + try { + ((Extension)extension).extensionAdded(this); + } + catch (Throwable e) { + LOG.error(e); + } + } + + notifyListenersOnAdd(extension, adapter.getPluginDescriptor()); + adapter.setNotificationSent(true); + } + } + } + + private void notifyListenersOnAdd(@NotNull T extension, final PluginDescriptor pluginDescriptor) { + for (ExtensionPointListener<T> listener : myEPListeners) { + try { + listener.extensionAdded(extension, pluginDescriptor); + } + catch (Throwable e) { + LOG.error(e); + } + } + } + + @NotNull + @Override + public List<T> getExtensionList() { + List<T> result = myExtensionsCache; + if (result == null) { + synchronized (this) { + result = myExtensionsCache; + if (result == null) { + T[] array = processAdapters(); + if (array == null) { + result = Collections.emptyList(); + } + else { + myExtensionsCacheAsArray = array; + result = Collections.unmodifiableList(Arrays.asList(array)); + } + myExtensionsCache = result; + } + } + } + return result; + } + + @Override + @NotNull + public T[] getExtensions() { + List<T> list = getExtensionList(); + if (list.isEmpty()) { + //noinspection unchecked + return (T[])Array.newInstance(getExtensionClass(), 0); + } + else { + return myExtensionsCacheAsArray.clone(); + } + } + + @NotNull + @Override + public Stream<T> extensions() { + return getExtensionList().stream(); + } + + @Override + public boolean hasAnyExtensions() { + final List<T> cache = myExtensionsCache; + if (cache != null) { + return !cache.isEmpty(); + } + synchronized (this) { + return myExtensionAdapters.size() + myLoadedAdapters.size() > 0; + } + } + + private boolean processingAdaptersNow; // guarded by this + @Nullable("null means empty") + private T[] processAdapters() { + if (processingAdaptersNow) { + throw new IllegalStateException("Recursive processAdapters() detected. You must have called 'getExtensions()' from within your extension constructor - don't. Either pass extension via constructor parameter or call getExtensions() later."); + } + int totalSize = myExtensionAdapters.size() + myLoadedAdapters.size(); + if (totalSize == 0) { + return null; + } + + processingAdaptersNow = true; + try { + Class<T> extensionClass = getExtensionClass(); + @SuppressWarnings("unchecked") T[] result = (T[])Array.newInstance(extensionClass, totalSize); + List<ExtensionComponentAdapter> adapters = ContainerUtil.newArrayListWithCapacity(totalSize); + adapters.addAll(myExtensionAdapters); + adapters.addAll(myLoadedAdapters); + LoadingOrder.sort(adapters); + myExtensionAdapters = new LinkedHashSet<>(adapters); + + Set<ExtensionComponentAdapter> loaded = ContainerUtil.newHashOrEmptySet(myLoadedAdapters); + + myLoadedAdapters = Collections.emptyList(); + boolean errorHappened = false; + for (int i = 0; i < adapters.size(); i++) { + ExtensionComponentAdapter adapter = adapters.get(i); + try { + @SuppressWarnings("unchecked") T extension = (T)adapter.getExtension(); + if (extension == null) { + errorHappened = true; + LOG.error("null extension in: " + adapter + ";\ngetExtensionClass(): " + getExtensionClass() + ";\n" ); + } + if (i > 0 && extension == result[i - 1]) { + errorHappened = true; + LOG.error("Duplicate extension found: " + extension + "; " + + " Adapter: " + adapter + ";\n" + + " Prev adapter: " + adapters.get(i-1) + ";\n" + + " getExtensionClass(): " + getExtensionClass() + ";\n" + + " result:" + Arrays.asList(result)); + } + if (!extensionClass.isInstance(extension)) { + errorHappened = true; + LOG.error("Extension " + (extension == null ? null : extension.getClass()) + " does not implement " + extensionClass + ". It came from " + adapter); + continue; + } + result[i] = extension; + registerExtension(extension, adapter, myLoadedAdapters.size(), !loaded.contains(adapter)); + } + catch (ProcessCanceledException e) { + throw e; + } + catch (Exception e) { + errorHappened = true; + LOG.error(e); + } + myExtensionAdapters.remove(adapter); + } + myExtensionAdapters = Collections.emptySet(); + if (errorHappened) { + result = ContainerUtil.findAllAsArray(result, Condition.NOT_NULL); + } + return result; + } + finally { + processingAdaptersNow = false; + } + } + + public synchronized void removeUnloadableExtensions() { + ExtensionComponentAdapter[] adapters = myExtensionAdapters.toArray(ExtensionComponentAdapter.EMPTY_ARRAY); + for (ExtensionComponentAdapter adapter : adapters) { + try { + adapter.getComponentImplementation(); + } + catch (Throwable e) { + unregisterExtensionAdapter(adapter); + } + } + } + + @Override + @Nullable + public T getExtension() { + List<T> extensions = getExtensionList(); + return extensions.isEmpty() ? null : extensions.get(0); + } + + @Override + public synchronized boolean hasExtension(@NotNull T extension) { + T[] extensions = processAdapters(); + return extensions != null && ArrayUtil.contains(extension, extensions); + } + + @Override + public synchronized void unregisterExtension(@NotNull final T extension) { + final int index = getExtensionIndex(extension); + if (index == -1) { + throw new IllegalArgumentException("Extension to be removed not found: " + extension); + } + final ExtensionComponentAdapter adapter = myLoadedAdapters.get(index); + + Object key = adapter.getComponentKey(); + myOwner.getPicoContainer().unregisterComponent(key); + + processAdapters(); + unregisterExtension(extension, null); + } + + @Override + public void unregisterExtension(@NotNull Class<? extends T> extensionClass) { + for (ExtensionComponentAdapter adapter : ContainerUtil.concat(myExtensionAdapters, myLoadedAdapters)) { + if (adapter.getAssignableToClassName().equals(extensionClass.getCanonicalName())) { + unregisterExtensionAdapter(adapter); + return; + } + } + throw new IllegalArgumentException("Extension to be removed not found: " + extensionClass); + } + + private int getExtensionIndex(@NotNull T extension) { + for (int i = 0; i < myLoadedAdapters.size(); i++) { + ExtensionComponentAdapter adapter = myLoadedAdapters.get(i); + if (Comparing.equal(adapter.getExtension(), extension)) return i; + } + return -1; + } + + private void unregisterExtension(@NotNull T extension, PluginDescriptor pluginDescriptor) { + int index = getExtensionIndex(extension); + if (index == -1) { + throw new IllegalArgumentException("Extension to be removed not found: " + extension); + } + + myLoadedAdapters.remove(index); + clearCache(); + + notifyListenersOnRemove(extension, pluginDescriptor); + + if (extension instanceof Extension) { + try { + ((Extension)extension).extensionRemoved(this); + } + catch (Throwable e) { + LOG.error(e); + } + } + } + + private void notifyListenersOnRemove(@NotNull T extensionObject, PluginDescriptor pluginDescriptor) { + for (ExtensionPointListener<T> listener : myEPListeners) { + try { + listener.extensionRemoved(extensionObject, pluginDescriptor); + } + catch (Throwable e) { + LOG.error(e); + } + } + } + + @Override + public void addExtensionPointListener(@NotNull final ExtensionPointListener<T> listener, @NotNull Disposable parentDisposable) { + addExtensionPointListener(listener, true, parentDisposable); + } + + public synchronized void addExtensionPointListener(@NotNull final ExtensionPointListener<T> listener, + final boolean invokeForLoadedExtensions, + @NotNull Disposable parentDisposable) { + if (invokeForLoadedExtensions) { + addExtensionPointListener(listener); + } + else { + addListener(listener); + } + Disposer.register(parentDisposable, () -> removeExtensionPointListener(listener, invokeForLoadedExtensions)); + } + + // true if added + private boolean addListener(@NotNull ExtensionPointListener<T> listener) { + if (ArrayUtil.indexOf(myEPListeners, listener) != -1) return false; + //noinspection unchecked + myEPListeners = ArrayUtil.append(myEPListeners, listener, n->n==0?ExtensionPointListener.EMPTY_ARRAY:new ExtensionPointListener[n]); + return true; + } + private boolean removeListener(@NotNull ExtensionPointListener<T> listener) { + if (ArrayUtil.indexOf(myEPListeners, listener) == -1) return false; + //noinspection unchecked + myEPListeners = ArrayUtil.remove(myEPListeners, listener, n->n==0?ExtensionPointListener.EMPTY_ARRAY:new ExtensionPointListener[n]); + return true; + } + + @Override + public synchronized void addExtensionPointListener(@NotNull ExtensionPointListener<T> listener) { + processAdapters(); + if (addListener(listener)) { + ExtensionComponentAdapter[] array = myLoadedAdapters.toArray(ExtensionComponentAdapter.EMPTY_ARRAY); + for (ExtensionComponentAdapter componentAdapter : array) { + try { + @SuppressWarnings("unchecked") T extension = (T)componentAdapter.getExtension(); + listener.extensionAdded(extension, componentAdapter.getPluginDescriptor()); + } + catch (Throwable e) { + LOG.error(e); + } + } + } + } + + @Override + public void removeExtensionPointListener(@NotNull ExtensionPointListener<T> listener) { + removeExtensionPointListener(listener, true); + } + + private synchronized void removeExtensionPointListener(@NotNull ExtensionPointListener<T> listener, boolean invokeForLoadedExtensions) { + if (removeListener(listener) && invokeForLoadedExtensions) { + ExtensionComponentAdapter[] array = myLoadedAdapters.toArray(ExtensionComponentAdapter.EMPTY_ARRAY); + for (ExtensionComponentAdapter componentAdapter : array) { + try { + @SuppressWarnings("unchecked") T extension = (T)componentAdapter.getExtension(); + listener.extensionRemoved(extension, componentAdapter.getPluginDescriptor()); + } + catch (Throwable e) { + LOG.error(e); + } + } + } + } + + @Override + public synchronized void reset() { + myOwner.removeAllComponents(myExtensionAdapters); + myExtensionAdapters = Collections.emptySet(); + for (T extension : getExtensionList()) { + unregisterExtension(extension); + } + } + + @NotNull + @Override + public Class<T> getExtensionClass() { + // racy single-check: we don't care whether the access to 'myExtensionClass' is thread-safe + // but initial store in a local variable is crucial to prevent instruction reordering + // see Item 71 in Effective Java or http://jeremymanson.blogspot.com/2008/12/benign-data-races-in-java.html + Class<T> extensionClass = myExtensionClass; + if (extensionClass == null) { + try { + ClassLoader pluginClassLoader = myDescriptor.getPluginClassLoader(); + @SuppressWarnings("unchecked") Class<T> extClass = + (Class<T>)(pluginClassLoader == null ? Class.forName(myClassName) : Class.forName(myClassName, true, pluginClassLoader)); + myExtensionClass = extensionClass = extClass; + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + return extensionClass; + } + + public String toString() { + return getName(); + } + + synchronized void registerExtensionAdapter(@NotNull ExtensionComponentAdapter adapter) { + if (myExtensionAdapters == Collections.<ExtensionComponentAdapter>emptySet()) { + myExtensionAdapters = new LinkedHashSet<>(); + } + myExtensionAdapters.add(adapter); + clearCache(); + } + + private void clearCache() { + myExtensionsCache = null; + myExtensionsCacheAsArray = null; + } + + private void unregisterExtensionAdapter(@NotNull ExtensionComponentAdapter adapter) { + try { + if (!myExtensionAdapters.isEmpty() && myExtensionAdapters.remove(adapter)) { + return; + } + if (myLoadedAdapters.contains(adapter)) { + Object key = adapter.getComponentKey(); + myOwner.getPicoContainer().unregisterComponent(key); + + @SuppressWarnings("unchecked") T extension = (T)adapter.getExtension(); + unregisterExtension(extension, adapter.getPluginDescriptor()); + } + } + finally { + clearCache(); + } + } + + @TestOnly + final synchronized void notifyAreaReplaced(@NotNull ExtensionsArea oldArea) { + for (final ExtensionPointListener<T> listener : myEPListeners) { + if (listener instanceof ExtensionPointAndAreaListener) { + ((ExtensionPointAndAreaListener)listener).areaReplaced(oldArea); + } + } + } + + private static class ObjectComponentAdapter extends ExtensionComponentAdapter { + private final Object myExtension; + private final LoadingOrder myLoadingOrder; + + private ObjectComponentAdapter(@NotNull Object extension, @NotNull LoadingOrder loadingOrder) { + super(extension.getClass().getName(), null, null, null); + myExtension = extension; + myLoadingOrder = loadingOrder; + } + + @Override + public Object getExtension() { + return myExtension; + } + + @Override + public LoadingOrder getOrder() { + return myLoadingOrder; + } + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionsAreaImpl.java b/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionsAreaImpl.java new file mode 100644 index 00000000..bcee0c07 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/impl/ExtensionsAreaImpl.java @@ -0,0 +1,406 @@ +// Copyright 2000-2018 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.extensions.impl; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.*; +import com.intellij.openapi.util.JDOMUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.MultiMap; +import com.intellij.util.pico.CachingConstructorInjectionComponentAdapter; +import com.intellij.util.pico.DefaultPicoContainer; +import gnu.trove.THashMap; +import org.jdom.Attribute; +import org.jdom.Element; +import org.jdom.Namespace; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; +import org.picocontainer.MutablePicoContainer; +import org.picocontainer.PicoContainer; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +@SuppressWarnings("HardCodedStringLiteral") +public class ExtensionsAreaImpl implements ExtensionsArea { + private static final Logger LOG = Logger.getInstance(ExtensionsAreaImpl.class); + public static final String ATTRIBUTE_AREA = "area"; + + private static final Map<String,String> ourDefaultEPs = new THashMap<>(); + + static { + ourDefaultEPs.put(EPAvailabilityListenerExtension.EXTENSION_POINT_NAME, EPAvailabilityListenerExtension.class.getName()); + } + + private static final boolean DEBUG_REGISTRATION = Boolean.FALSE.booleanValue(); // not compile-time constant to avoid yellow code + + private final AreaPicoContainer myPicoContainer; + private final Throwable myCreationTrace; + private final Map<String, ExtensionPointImpl> myExtensionPoints = ContainerUtil.newConcurrentMap(); + private final Map<String,Throwable> myEPTraces = DEBUG_REGISTRATION ? new THashMap<>() : null; + private final MultiMap<String, ExtensionPointAvailabilityListener> myAvailabilityListeners = MultiMap.createSmart(); // guarded by myAvailabilityListeners + private final AreaInstance myAreaInstance; + private final String myAreaClass; + + public ExtensionsAreaImpl(String areaClass, AreaInstance areaInstance, PicoContainer parentPicoContainer) { + myCreationTrace = DEBUG_REGISTRATION ? new Throwable("Area creation trace") : null; + myAreaClass = areaClass; + myAreaInstance = areaInstance; + myPicoContainer = new DefaultPicoContainer(parentPicoContainer); + initialize(); + } + + @TestOnly + ExtensionsAreaImpl(MutablePicoContainer parentPicoContainer) { + this(null, null, parentPicoContainer); + } + + @TestOnly + public final void notifyAreaReplaced(@NotNull ExtensionsAreaImpl newArea) { + Set<String> processedEPs = ContainerUtil.newTroveSet(); + for (final ExtensionPointImpl point : myExtensionPoints.values()) { + point.notifyAreaReplaced(this); + processedEPs.add(point.getName()); + } + //this code is required because we have a lot of static extensions e.g. LanguageExtension that are initialized only once + //for the extensions AvailabilityListeners will be broken if the initialization happened in "fake" area which doesn't have required EP + if (!myAvailabilityListeners.isEmpty()) { + for (Map.Entry<String, Collection<ExtensionPointAvailabilityListener>> entry : myAvailabilityListeners.entrySet()) { + String key = entry.getKey(); + if (!processedEPs.contains(key)) { + boolean wasAdded = false; + //if listeners are "detached" for any EP we have to transfer them to the new area (otherwise it will affect area searching) + for (ExtensionPointAvailabilityListener listener : entry.getValue()) { + if (!newArea.hasAvailabilityListener(key, listener)) { + newArea.addAvailabilityListener(key, listener); + wasAdded = true; + } + } + if (wasAdded) { + processedEPs.add(key); + } + } + } + } + + for (ExtensionPointImpl point : newArea.myExtensionPoints.values()) { + if (!processedEPs.contains(point.getName())) { + point.notifyAreaReplaced(this); + } + } + } + + + @NotNull + @Override + public AreaPicoContainer getPicoContainer() { + return myPicoContainer; + } + + @Override + public String getAreaClass() { + return myAreaClass; + } + + public void registerExtensionPoint(@NotNull String pluginName, @NotNull Element extensionPointElement) { + registerExtensionPoint(new DefaultPluginDescriptor(PluginId.getId(pluginName)), extensionPointElement); + } + + @Override + public void registerExtensionPoint(@NotNull PluginDescriptor pluginDescriptor, @NotNull Element extensionPointElement) { + assert pluginDescriptor.getPluginId() != null; + final String pluginId = pluginDescriptor.getPluginId().getIdString(); + String epName = extensionPointElement.getAttributeValue("qualifiedName"); + if (epName == null) { + final String name = extensionPointElement.getAttributeValue("name"); + if (name == null) { + throw new RuntimeException("'name' attribute not specified for extension point in '" + pluginId + "' plugin"); + } + epName = pluginId + '.' + name; + } + + String beanClassName = extensionPointElement.getAttributeValue("beanClass"); + String interfaceClassName = extensionPointElement.getAttributeValue("interface"); + if (beanClassName == null && interfaceClassName == null) { + throw new RuntimeException("Neither 'beanClass' nor 'interface' attribute is specified for extension point '" + epName + "' in '" + pluginId + "' plugin"); + } + if (beanClassName != null && interfaceClassName != null) { + throw new RuntimeException("Both 'beanClass' and 'interface' attributes are specified for extension point '" + epName + "' in '" + pluginId + "' plugin"); + } + + ExtensionPoint.Kind kind; + String className; + if (interfaceClassName != null) { + className = interfaceClassName; + kind = ExtensionPoint.Kind.INTERFACE; + } + else { + className = beanClassName; + kind = ExtensionPoint.Kind.BEAN_CLASS; + } + registerExtensionPoint(epName, className, pluginDescriptor, kind); + } + + public void registerExtension(@NotNull final String pluginName, @NotNull final Element extensionElement) { + registerExtension(new DefaultPluginDescriptor(PluginId.getId(pluginName)), extensionElement, null); + } + + @Override + public void registerExtension(@NotNull final PluginDescriptor pluginDescriptor, @NotNull final Element extensionElement, String extensionNs) { + String epName = extractEPName(extensionElement, extensionNs); + registerExtension(getExtensionPoint(epName), pluginDescriptor, extensionElement); + } + + // Used in Upsource + @Override + public void registerExtension(@NotNull final ExtensionPoint extensionPoint, @NotNull final PluginDescriptor pluginDescriptor, @NotNull final Element extensionElement) { + if (!Extensions.isComponentSuitableForOs(extensionElement.getAttributeValue("os"))) { + return; + } + + ExtensionComponentAdapter adapter; + if (extensionPoint.getKind() == ExtensionPoint.Kind.INTERFACE) { + String implClass = extensionElement.getAttributeValue("implementation"); + if (implClass == null) { + throw new RuntimeException("'implementation' attribute not specified for '" + extensionPoint.getName() + "' extension in '" + + pluginDescriptor.getPluginId().getIdString() + "' plugin"); + } + adapter = createAdapter(implClass, extensionElement, shouldDeserializeInstance(extensionElement), pluginDescriptor); + } + else { + adapter = createAdapter(extensionPoint.getClassName(), extensionElement, !JDOMUtil.isEmpty(extensionElement), pluginDescriptor); + } + myPicoContainer.registerComponent(adapter); + ((ExtensionPointImpl)extensionPoint).registerExtensionAdapter(adapter); + } + + // this method is not ExtensionComponentAdapter constructor because later ExtensionComponentAdapter will not hold element + @NotNull + private ExtensionComponentAdapter createAdapter(@NotNull String implementationClassName, @NotNull Element extensionElement, boolean isNeedToDeserialize, @NotNull PluginDescriptor pluginDescriptor) { + String orderId = extensionElement.getAttributeValue("id"); + LoadingOrder order = LoadingOrder.readOrder(extensionElement.getAttributeValue("order")); + return new ExtensionComponentAdapter(implementationClassName, myPicoContainer, pluginDescriptor, orderId, order, isNeedToDeserialize ? extensionElement : null); + } + + private static boolean shouldDeserializeInstance(Element extensionElement) { + // has content + if (!extensionElement.getContent().isEmpty()) return true; + // has custom attributes + for (Attribute attribute : extensionElement.getAttributes()) { + final String name = attribute.getName(); + if (!"implementation".equals(name) && !"id".equals(name) && !"order".equals(name) && !"os".equals(name)) { + return true; + } + } + return false; + } + + @NotNull + public static String extractEPName(@NotNull Element extensionElement, @Nullable String ns) { + String epName = extensionElement.getAttributeValue("point"); + + if (epName == null) { + if (ns == null) { + Namespace namespace = extensionElement.getNamespace(); + epName = namespace.getURI() + '.' + extensionElement.getName(); + } + else { + epName = ns + '.' + extensionElement.getName(); + } + } + return epName; + } + + private MutablePicoContainer internalGetPluginContainer() { + return myPicoContainer; + } + + @SuppressWarnings("unchecked") + private void initialize() { + for (Map.Entry<String, String> entry : ourDefaultEPs.entrySet()) { + String epName = entry.getKey(); + registerExtensionPoint(epName, entry.getValue()); + } + + getExtensionPoint(EPAvailabilityListenerExtension.EXTENSION_POINT_NAME).addExtensionPointListener(new ExtensionPointListener() { + @Override + public void extensionRemoved(@NotNull Object extension, final PluginDescriptor pluginDescriptor) { + EPAvailabilityListenerExtension epListenerExtension = (EPAvailabilityListenerExtension) extension; + synchronized (myAvailabilityListeners) { + Collection<ExtensionPointAvailabilityListener> listeners = myAvailabilityListeners.get(epListenerExtension.getExtensionPointName()); + for (Iterator<ExtensionPointAvailabilityListener> iterator = listeners.iterator(); iterator.hasNext();) { + ExtensionPointAvailabilityListener listener = iterator.next(); + if (listener.getClass().getName().equals(epListenerExtension.getListenerClass())) { + iterator.remove(); + return; + } + } + } + LOG.warn("Failed to find EP availability listener: " + epListenerExtension.getListenerClass()); + } + + @Override + public void extensionAdded(@NotNull Object extension, final PluginDescriptor pluginDescriptor) { + EPAvailabilityListenerExtension epListenerExtension = (EPAvailabilityListenerExtension) extension; + try { + String epName = epListenerExtension.getExtensionPointName(); + + ExtensionPointAvailabilityListener listener = (ExtensionPointAvailabilityListener) instantiate(epListenerExtension.loadListenerClass()); + addAvailabilityListener(epName, listener); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + + private Object instantiate(Class clazz) { + CachingConstructorInjectionComponentAdapter adapter = + new CachingConstructorInjectionComponentAdapter(Integer.toString(System.identityHashCode(new Object())), clazz); + + return adapter.getComponentInstance(getPicoContainer()); + } + + @SuppressWarnings("UnusedDeclaration") + public Throwable getCreationTrace() { + return myCreationTrace; + } + + @Override + public void addAvailabilityListener(@NotNull String extensionPointName, @NotNull ExtensionPointAvailabilityListener listener) { + synchronized (myAvailabilityListeners) { + myAvailabilityListeners.putValue(extensionPointName, listener); + } + ExtensionPointImpl<?> ep = myExtensionPoints.get(extensionPointName); + if (ep != null) { + listener.extensionPointRegistered(ep); + } + } + + @Override + public void removeAvailabilityListener(@NotNull String extensionPointName, @NotNull ExtensionPointAvailabilityListener listener) { + synchronized (myAvailabilityListeners) { + myAvailabilityListeners.remove(extensionPointName, listener); + } + } + + private boolean hasAvailabilityListener(@NotNull String extensionPointName, @NotNull ExtensionPointAvailabilityListener listener) { + Collection<ExtensionPointAvailabilityListener> listeners = myAvailabilityListeners.get(extensionPointName); + return ContainerUtil.containsIdentity(listeners, listener); + } + + @Override + public void registerExtensionPoint(@NotNull final String extensionPointName, @NotNull String extensionPointBeanClass) { + registerExtensionPoint(extensionPointName, extensionPointBeanClass, ExtensionPoint.Kind.INTERFACE); + } + + @Override + public void registerExtensionPoint(@NotNull @NonNls String extensionPointName, @NotNull String extensionPointBeanClass, @NotNull ExtensionPoint.Kind kind) { + registerExtensionPoint(extensionPointName, extensionPointBeanClass, new UndefinedPluginDescriptor(), kind); + } + + private void registerExtensionPoint(@NotNull String extensionPointName, + @NotNull String extensionPointBeanClass, + @NotNull PluginDescriptor descriptor, + @NotNull ExtensionPoint.Kind kind) { + if (hasExtensionPoint(extensionPointName)) { + final String message = + "Duplicate registration for EP: " + extensionPointName + ": original plugin " + getExtensionPoint(extensionPointName).getDescriptor().getPluginId() + + ", new plugin " + descriptor.getPluginId(); + if (DEBUG_REGISTRATION) { + LOG.error(message, myEPTraces.get(extensionPointName)); + } + throw new PicoPluginExtensionInitializationException(message, null, descriptor.getPluginId()); + } + + registerExtensionPoint(new ExtensionPointImpl(extensionPointName, extensionPointBeanClass, kind, this, myAreaInstance, descriptor)); + } + + public void registerExtensionPoint(@NotNull ExtensionPointImpl extensionPoint) { + String name = extensionPoint.getName(); + myExtensionPoints.put(name, extensionPoint); + notifyEPRegistered(extensionPoint); + if (DEBUG_REGISTRATION) { + myEPTraces.put(name, new Throwable("Original registration for " + name)); + } + } + + private void notifyEPRegistered(@NotNull ExtensionPoint extensionPoint) { + Collection<ExtensionPointAvailabilityListener> listeners; + synchronized (myAvailabilityListeners) { + listeners = myAvailabilityListeners.get(extensionPoint.getName()); + } + for (final ExtensionPointAvailabilityListener listener : listeners) { + listener.extensionPointRegistered(extensionPoint); + } + } + + @Override + @NotNull + public <T> ExtensionPointImpl<T> getExtensionPoint(@NotNull String extensionPointName) { + //noinspection unchecked + ExtensionPointImpl<T> extensionPoint = myExtensionPoints.get(extensionPointName); + if (extensionPoint == null) { + throw new IllegalArgumentException("Missing extension point: " + extensionPointName + " in area " + myAreaInstance); + } + return extensionPoint; + } + + @NotNull + @Override + public <T> ExtensionPoint<T> getExtensionPoint(@NotNull ExtensionPointName<T> extensionPointName) { + return getExtensionPoint(extensionPointName.getName()); + } + + @NotNull + @Override + public ExtensionPoint[] getExtensionPoints() { + return myExtensionPoints.values().toArray(new ExtensionPoint[0]); + } + + @Override + public void unregisterExtensionPoint(@NotNull final String extensionPointName) { + ExtensionPoint extensionPoint = myExtensionPoints.get(extensionPointName); + if (extensionPoint != null) { + extensionPoint.reset(); + myExtensionPoints.remove(extensionPointName); + notifyEPRemoved(extensionPoint); + } + } + + private void notifyEPRemoved(@NotNull ExtensionPoint extensionPoint) { + Collection<ExtensionPointAvailabilityListener> listeners; + synchronized (myAvailabilityListeners) { + listeners = myAvailabilityListeners.get(extensionPoint.getName()); + } + for (final ExtensionPointAvailabilityListener listener : listeners) { + listener.extensionPointRemoved(extensionPoint); + } + } + + @Override + public boolean hasExtensionPoint(@NotNull String extensionPointName) { + return myExtensionPoints.containsKey(extensionPointName); + } + + @Override + public boolean hasExtensionPoint(@NotNull ExtensionPointName<?> extensionPointName) { + return hasExtensionPoint(extensionPointName.getName()); + } + + void removeAllComponents(@NotNull Set<ExtensionComponentAdapter> extensionAdapters) { + for (final Object extensionAdapter : extensionAdapters) { + ExtensionComponentAdapter componentAdapter = (ExtensionComponentAdapter)extensionAdapter; + internalGetPluginContainer().unregisterComponent(componentAdapter.getComponentKey()); + } + } + + @Override + public String toString() { + return (myAreaClass == null ? "Root" : myAreaClass)+" Area"; + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/impl/PicoPluginExtensionInitializationException.java b/platform/extensions/src/com/intellij/openapi/extensions/impl/PicoPluginExtensionInitializationException.java new file mode 100644 index 00000000..4e609132 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/impl/PicoPluginExtensionInitializationException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2000-2013 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 com.intellij.openapi.extensions.impl; + +import com.intellij.openapi.extensions.PluginId; +import org.picocontainer.PicoInitializationException; + +public class PicoPluginExtensionInitializationException extends PicoInitializationException { + private final PluginId myPluginId; + + PicoPluginExtensionInitializationException(String message, Throwable cause, PluginId id) { + super(message, cause); + myPluginId = id; + } + + public PluginId getPluginId() { + return myPluginId; + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/impl/UndefinedPluginDescriptor.java b/platform/extensions/src/com/intellij/openapi/extensions/impl/UndefinedPluginDescriptor.java new file mode 100644 index 00000000..4ab748c1 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/impl/UndefinedPluginDescriptor.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions.impl; + +import com.intellij.openapi.extensions.PluginDescriptor; +import com.intellij.openapi.extensions.PluginId; + +/** + * @author Alexander Kireyev + */ +class UndefinedPluginDescriptor implements PluginDescriptor { + @Override + public PluginId getPluginId() { + throw new UnsupportedOperationException("This method should not be called on this object"); + } + + @Override + public ClassLoader getPluginClassLoader() { + return null; + } +} diff --git a/platform/extensions/src/com/intellij/openapi/extensions/impl/package.html b/platform/extensions/src/com/intellij/openapi/extensions/impl/package.html new file mode 100644 index 00000000..56fe0777 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/impl/package.html @@ -0,0 +1,4 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html><body bgcolor="white"> +Provides the implementation of the extension point framework used in IDEA and Fabrique. +</body></html> diff --git a/platform/extensions/src/com/intellij/openapi/extensions/package.html b/platform/extensions/src/com/intellij/openapi/extensions/package.html new file mode 100644 index 00000000..6dccaee7 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/extensions/package.html @@ -0,0 +1,4 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html><body bgcolor="white"> +Provides interfaces for the extension point framework used in IDEA and Fabrique. +</body></html> diff --git a/platform/extensions/src/com/intellij/openapi/util/KeyedExtensionFactory.java b/platform/extensions/src/com/intellij/openapi/util/KeyedExtensionFactory.java new file mode 100644 index 00000000..cbe19884 --- /dev/null +++ b/platform/extensions/src/com/intellij/openapi/util/KeyedExtensionFactory.java @@ -0,0 +1,114 @@ +// Copyright 2000-2018 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.util; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.extensions.KeyedFactoryEPBean; +import com.intellij.util.ExceptionUtil; +import gnu.trove.THashSet; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.picocontainer.PicoContainer; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Set; + +/** + * @author yole + */ +public abstract class KeyedExtensionFactory<T, KeyT> { + private final Class<T> myInterfaceClass; + private final ExtensionPointName<KeyedFactoryEPBean> myEpName; + private final PicoContainer myPicoContainer; + + public KeyedExtensionFactory(@NotNull final Class<T> interfaceClass, @NonNls @NotNull final ExtensionPointName<KeyedFactoryEPBean> epName, + @NotNull PicoContainer picoContainer) { + myInterfaceClass = interfaceClass; + myEpName = epName; + myPicoContainer = picoContainer; + } + + @NotNull + public T get() { + final List<KeyedFactoryEPBean> epBeans = myEpName.getExtensionList(); + InvocationHandler handler = new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + //noinspection unchecked + KeyT keyArg = (KeyT) args [0]; + String key = getKey(keyArg); + Object result = getByKey(epBeans, key, method, args); + if (result == null) { + result = getByKey(epBeans, null, method, args); + } + return result; + } + }; + //noinspection unchecked + return (T)Proxy.newProxyInstance(myInterfaceClass.getClassLoader(), new Class<?>[] { myInterfaceClass }, handler ); + } + + public T getByKey(@NotNull KeyT key) { + final List<KeyedFactoryEPBean> epBeans = myEpName.getExtensionList(); + for (KeyedFactoryEPBean epBean : epBeans) { + if (Comparing.strEqual(getKey(key), epBean.key)) { + try { + if (epBean.implementationClass != null) { + return (T)epBean.instantiate(epBean.implementationClass, myPicoContainer); + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + } + return null; + } + + @NotNull + public Set<String> getAllKeys() { + List<KeyedFactoryEPBean> list = myEpName.getExtensionList(); + Set<String> set = new THashSet<>(); + for (KeyedFactoryEPBean epBean : list) { + set.add(epBean.key); + } + return set; + } + + private T getByKey(final List<KeyedFactoryEPBean> epBeans, final String key, final Method method, final Object[] args) { + Object result = null; + for(KeyedFactoryEPBean epBean: epBeans) { + if (Comparing.strEqual(epBean.key, key, true)) { + try { + if (epBean.implementationClass != null) { + result = epBean.instantiate(epBean.implementationClass, myPicoContainer); + } + else { + Object factory = epBean.instantiate(epBean.factoryClass, myPicoContainer); + result = method.invoke(factory, args); + } + if (result != null) { + break; + } + } + catch (InvocationTargetException e) { + ExceptionUtil.rethrowUnchecked(e.getCause()); + throw new RuntimeException(e); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + } + return (T)result; + } + + public abstract String getKey(@NotNull KeyT key); +} + diff --git a/platform/extensions/src/com/intellij/util/pico/AssignableToComponentAdapter.java b/platform/extensions/src/com/intellij/util/pico/AssignableToComponentAdapter.java new file mode 100644 index 00000000..6ca5d286 --- /dev/null +++ b/platform/extensions/src/com/intellij/util/pico/AssignableToComponentAdapter.java @@ -0,0 +1,23 @@ +/* + * Copyright 2000-2009 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 com.intellij.util.pico; + +import org.picocontainer.ComponentAdapter; + +public interface AssignableToComponentAdapter extends ComponentAdapter { + String getAssignableToClassName(); +} diff --git a/platform/extensions/src/com/intellij/util/pico/CachingConstructorInjectionComponentAdapter.java b/platform/extensions/src/com/intellij/util/pico/CachingConstructorInjectionComponentAdapter.java new file mode 100644 index 00000000..baa2a41c --- /dev/null +++ b/platform/extensions/src/com/intellij/util/pico/CachingConstructorInjectionComponentAdapter.java @@ -0,0 +1,200 @@ +// Copyright 2000-2018 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.util.pico; + +import com.intellij.util.ExceptionUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.picocontainer.*; +import org.picocontainer.defaults.*; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; + +/** + * A drop-in replacement of {@link ConstructorInjectionComponentAdapter} + * The same code (generified and cleaned up) but without constructor caching (hence taking up less memory). + * This class also inlines instance caching (e.g. it doesn't need to be wrapped in a CachingComponentAdapter). + */ +public class CachingConstructorInjectionComponentAdapter extends InstantiatingComponentAdapter { + @SuppressWarnings("SSBasedInspection") + private static final ThreadLocal<Set<CachingConstructorInjectionComponentAdapter>> ourGuard = + new ThreadLocal<>(); + private Object myInstance; + + public CachingConstructorInjectionComponentAdapter(@NotNull Object componentKey, @NotNull Class componentImplementation, Parameter[] parameters, boolean allowNonPublicClasses) throws AssignabilityRegistrationException, NotConcreteRegistrationException { + super(componentKey, componentImplementation, parameters, allowNonPublicClasses, DefaultPicoContainer.DEFAULT_DELEGATING_COMPONENT_MONITOR, DefaultPicoContainer.DEFAULT_LIFECYCLE_STRATEGY); + } + + public CachingConstructorInjectionComponentAdapter(@NotNull Object componentKey, @NotNull Class componentImplementation, Parameter[] parameters) { + this(componentKey, componentImplementation, parameters, false); + } + + public CachingConstructorInjectionComponentAdapter(@NotNull Object componentKey, @NotNull Class componentImplementation) throws AssignabilityRegistrationException, NotConcreteRegistrationException { + this(componentKey, componentImplementation, null); + } + + @Override + public Object getComponentInstance(PicoContainer container) throws PicoInitializationException, PicoIntrospectionException, + AssignabilityRegistrationException, NotConcreteRegistrationException { + Object instance = myInstance; + if (instance == null) { + myInstance = instance = instantiateGuarded(container, getComponentImplementation()); + } + return instance; + } + + private Object instantiateGuarded(PicoContainer container, Class stackFrame) { + Set<CachingConstructorInjectionComponentAdapter> currentStack = ourGuard.get(); + if (currentStack == null) { + ourGuard.set(currentStack = ContainerUtil.newIdentityTroveSet()); + } + + if (currentStack.contains(this)) { + throw new CyclicDependencyException(stackFrame); + } + + try { + currentStack.add(this); + return doGetComponentInstance(container); + } catch (final CyclicDependencyException e) { + e.push(stackFrame); + throw e; + } finally { + currentStack.remove(this); + } + } + + private Object doGetComponentInstance(PicoContainer guardedContainer) { + final Constructor constructor; + try { + constructor = getGreediestSatisfiableConstructor(guardedContainer); + } + catch (AmbiguousComponentResolutionException e) { + e.setComponent(getComponentImplementation()); + throw e; + } + ComponentMonitor componentMonitor = currentMonitor(); + try { + Object[] parameters = getConstructorArguments(guardedContainer, constructor); + componentMonitor.instantiating(constructor); + long startTime = System.currentTimeMillis(); + Object inst = newInstance(constructor, parameters); + componentMonitor.instantiated(constructor, System.currentTimeMillis() - startTime); + return inst; + } + catch (InvocationTargetException e) { + componentMonitor.instantiationFailed(constructor, e); + ExceptionUtil.rethrowUnchecked(e.getTargetException()); + throw new PicoInvocationTargetInitializationException(e.getTargetException()); + } + catch (InstantiationException e) { + componentMonitor.instantiationFailed(constructor, e); + throw new PicoInitializationException("Should never get here"); + } + catch (IllegalAccessException e) { + componentMonitor.instantiationFailed(constructor, e); + throw new PicoInitializationException(e); + } + } + + @NotNull + private Object[] getConstructorArguments(PicoContainer container, Constructor ctor) { + Class[] parameterTypes = ctor.getParameterTypes(); + Object[] result = new Object[parameterTypes.length]; + Parameter[] currentParameters = parameters != null ? parameters : createDefaultParameters(parameterTypes); + + for (int i = 0; i < currentParameters.length; i++) { + result[i] = currentParameters[i].resolveInstance(container, this, parameterTypes[i]); + } + return result; + } + + @Override + protected Constructor getGreediestSatisfiableConstructor(PicoContainer container) throws + PicoIntrospectionException, + AssignabilityRegistrationException, NotConcreteRegistrationException { + final Set<Constructor> conflicts = new HashSet<>(); + final Set<List<Class>> unsatisfiableDependencyTypes = new HashSet<>(); + List<Constructor> sortedMatchingConstructors = getSortedMatchingConstructors(); + Constructor greediestConstructor = null; + int lastSatisfiableConstructorSize = -1; + Class unsatisfiedDependencyType = null; + for (Constructor constructor : sortedMatchingConstructors) { + boolean failedDependency = false; + Class[] parameterTypes = constructor.getParameterTypes(); + Parameter[] currentParameters = parameters != null ? parameters : createDefaultParameters(parameterTypes); + + // remember: all constructors with less arguments than the given parameters are filtered out already + for (int j = 0; j < currentParameters.length; j++) { + // check whether this constructor is satisfiable + if (currentParameters[j].isResolvable(container, this, parameterTypes[j])) { + continue; + } + unsatisfiableDependencyTypes.add(Arrays.asList(parameterTypes)); + unsatisfiedDependencyType = parameterTypes[j]; + failedDependency = true; + break; + } + + if (greediestConstructor != null && parameterTypes.length != lastSatisfiableConstructorSize) { + if (conflicts.isEmpty()) { + // we found our match [aka. greedy and satisfied] + return greediestConstructor; + } + else { + // fits although not greedy + conflicts.add(constructor); + } + } + else if (!failedDependency && lastSatisfiableConstructorSize == parameterTypes.length) { + // satisfied and same size as previous one? + conflicts.add(constructor); + conflicts.add(greediestConstructor); + } + else if (!failedDependency) { + greediestConstructor = constructor; + lastSatisfiableConstructorSize = parameterTypes.length; + } + } + if (!conflicts.isEmpty()) { + throw new TooManySatisfiableConstructorsException(getComponentImplementation(), conflicts); + } else if (greediestConstructor == null && !unsatisfiableDependencyTypes.isEmpty()) { + throw new UnsatisfiableDependenciesException(this, unsatisfiedDependencyType, unsatisfiableDependencyTypes, container); + } else if (greediestConstructor == null) { + // be nice to the user, show all constructors that were filtered out + final Set<Constructor> nonMatching = ContainerUtil.newHashSet(getConstructors()); + throw new PicoInitializationException("Either do the specified parameters not match any of the following constructors: " + nonMatching.toString() + " or the constructors were not accessible for '" + getComponentImplementation() + "'"); + } + return greediestConstructor; + } + + private List<Constructor> getSortedMatchingConstructors() { + List<Constructor> matchingConstructors = new ArrayList<>(); + // filter out all constructors that will definitely not match + for (Constructor constructor : getConstructors()) { + if ((parameters == null || constructor.getParameterTypes().length == parameters.length) && + (allowNonPublicClasses || (constructor.getModifiers() & Modifier.PUBLIC) != 0)) { + matchingConstructors.add(constructor); + } + } + // optimize list of constructors moving the longest at the beginning + if (parameters == null) { + matchingConstructors.sort((arg0, arg1) -> arg1.getParameterTypes().length - arg0.getParameterTypes().length); + } + return matchingConstructors; + } + + @NotNull + private Constructor[] getConstructors() { + return (Constructor[]) AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + return getComponentImplementation().getDeclaredConstructors(); + } + }); + } +}
\ No newline at end of file diff --git a/platform/extensions/src/com/intellij/util/pico/DefaultPicoContainer.java b/platform/extensions/src/com/intellij/util/pico/DefaultPicoContainer.java new file mode 100644 index 00000000..c9e0c548 --- /dev/null +++ b/platform/extensions/src/com/intellij/util/pico/DefaultPicoContainer.java @@ -0,0 +1,407 @@ +// Copyright 2000-2018 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.util.pico; + +import com.intellij.openapi.extensions.AreaPicoContainer; +import com.intellij.util.ReflectionUtil; +import com.intellij.util.SmartList; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.FList; +import gnu.trove.THashSet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.picocontainer.*; +import org.picocontainer.defaults.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +public class DefaultPicoContainer implements AreaPicoContainer { + static final DelegatingComponentMonitor DEFAULT_DELEGATING_COMPONENT_MONITOR = new DelegatingComponentMonitor(); + static final DefaultLifecycleStrategy DEFAULT_LIFECYCLE_STRATEGY = new DefaultLifecycleStrategy(DEFAULT_DELEGATING_COMPONENT_MONITOR); + private final PicoContainer parent; + private final Set<PicoContainer> children = new THashSet<>(); + + private final Map<Object, ComponentAdapter> componentKeyToAdapterCache = ContainerUtil.newConcurrentMap(); + private final LinkedHashSetWrapper<ComponentAdapter> componentAdapters = new LinkedHashSetWrapper<>(); + private final Map<String, ComponentAdapter> classNameToAdapter = ContainerUtil.newConcurrentMap(); + private final AtomicReference<FList<ComponentAdapter>> nonAssignableComponentAdapters = new AtomicReference<>(FList.emptyList()); + + public DefaultPicoContainer(@Nullable PicoContainer parent) { + this.parent = parent == null ? null : ImmutablePicoContainerProxyFactory.newProxyInstance(parent); + } + + public DefaultPicoContainer() { + this(null); + } + + @Override + public Collection<ComponentAdapter> getComponentAdapters() { + return componentAdapters.getImmutableSet(); + } + + private void appendNonAssignableAdaptersOfType(@NotNull Class componentType, @NotNull List<? super ComponentAdapter> result) { + List<ComponentAdapter> comp = new ArrayList<>(); + for (final ComponentAdapter componentAdapter : nonAssignableComponentAdapters.get()) { + if (ReflectionUtil.isAssignable(componentType, componentAdapter.getComponentImplementation())) { + comp.add(componentAdapter); + } + } + for (int i = comp.size() - 1; i >= 0; i--) { + result.add(comp.get(i)); + } + } + + @Override + @Nullable + public final ComponentAdapter getComponentAdapter(Object componentKey) { + ComponentAdapter adapter = getFromCache(componentKey); + if (adapter == null && parent != null) { + return parent.getComponentAdapter(componentKey); + } + return adapter; + } + + @Nullable + private ComponentAdapter getFromCache(final Object componentKey) { + ComponentAdapter adapter = componentKeyToAdapterCache.get(componentKey); + if (adapter != null) { + return adapter; + } + + if (componentKey instanceof Class) { + return componentKeyToAdapterCache.get(((Class)componentKey).getName()); + } + + return null; + } + + @Override + @Nullable + public ComponentAdapter getComponentAdapterOfType(@NotNull Class componentType) { + // See http://jira.codehaus.org/secure/ViewIssue.jspa?key=PICO-115 + ComponentAdapter adapterByKey = getComponentAdapter(componentType); + if (adapterByKey != null) { + return adapterByKey; + } + + List<ComponentAdapter> found = getComponentAdaptersOfType(componentType); + if (found.size() == 1) { + return found.get(0); + } + if (found.isEmpty()) { + return parent == null ? null : parent.getComponentAdapterOfType(componentType); + } + + Class[] foundClasses = new Class[found.size()]; + for (int i = 0; i < foundClasses.length; i++) { + foundClasses[i] = found.get(i).getComponentImplementation(); + } + throw new AmbiguousComponentResolutionException(componentType, foundClasses); + } + + @Override + public List<ComponentAdapter> getComponentAdaptersOfType(final Class componentType) { + if (componentType == null || componentType == String.class) { + return Collections.emptyList(); + } + + List<ComponentAdapter> result = new SmartList<>(); + + final ComponentAdapter cacheHit = classNameToAdapter.get(componentType.getName()); + if (cacheHit != null) { + result.add(cacheHit); + } + + appendNonAssignableAdaptersOfType(componentType, result); + return result; + } + + @Override + public ComponentAdapter registerComponent(@NotNull ComponentAdapter componentAdapter) { + Object componentKey = componentAdapter.getComponentKey(); + if (componentKeyToAdapterCache.containsKey(componentKey)) { + throw new DuplicateComponentKeyRegistrationException(componentKey); + } + + if (componentAdapter instanceof AssignableToComponentAdapter) { + String classKey = ((AssignableToComponentAdapter)componentAdapter).getAssignableToClassName(); + classNameToAdapter.put(classKey, componentAdapter); + } + else { + do { + FList<ComponentAdapter> oldList = nonAssignableComponentAdapters.get(); + FList<ComponentAdapter> newList = oldList.prepend(componentAdapter); + if (nonAssignableComponentAdapters.compareAndSet(oldList, newList)) { + break; + } + } + while (true); + } + + componentAdapters.add(componentAdapter); + + componentKeyToAdapterCache.put(componentKey, componentAdapter); + return componentAdapter; + } + + @Override + public ComponentAdapter unregisterComponent(@NotNull Object componentKey) { + ComponentAdapter adapter = componentKeyToAdapterCache.remove(componentKey); + componentAdapters.remove(adapter); + if (adapter instanceof AssignableToComponentAdapter) { + classNameToAdapter.remove(((AssignableToComponentAdapter)adapter).getAssignableToClassName()); + } + else { + do { + FList<ComponentAdapter> oldList = nonAssignableComponentAdapters.get(); + FList<ComponentAdapter> newList = oldList.without(adapter); + if (nonAssignableComponentAdapters.compareAndSet(oldList, newList)) { + break; + } + } + while (true); + } + return adapter; + } + + @Override + public List getComponentInstances() { + return getComponentInstancesOfType(Object.class); + } + + @Override + public List<Object> getComponentInstancesOfType(@Nullable Class componentType) { + if (componentType == null) { + return Collections.emptyList(); + } + + List<Object> result = new ArrayList<>(); + for (ComponentAdapter componentAdapter : getComponentAdapters()) { + if (ReflectionUtil.isAssignable(componentType, componentAdapter.getComponentImplementation())) { + // may be null in the case of the "implicit" adapter representing "this". + ContainerUtil.addIfNotNull(result, getInstance(componentAdapter)); + } + } + return result; + } + + @FunctionalInterface + public interface LazyComponentAdapter { + boolean isComponentInstantiated(); + } + + @Nullable + public <T> T getComponentInstanceIfInstantiated(@NotNull String componentKey) { + ComponentAdapter adapter = getFromCache(componentKey); + if (!(adapter instanceof LazyComponentAdapter)) { + //noinspection unchecked + return (T)getComponentInstance(componentKey); + } + + if (((LazyComponentAdapter)adapter).isComponentInstantiated()) { + //noinspection unchecked + return (T)getLocalInstance(adapter); + } + return null; + } + + @Override + @Nullable + public Object getComponentInstance(Object componentKey) { + ComponentAdapter adapter = getFromCache(componentKey); + if (adapter != null) { + return getLocalInstance(adapter); + } + if (parent != null) { + adapter = parent.getComponentAdapter(componentKey); + if (adapter != null) { + return parent.getComponentInstance(adapter.getComponentKey()); + } + } + return null; + } + + @Override + @Nullable + public Object getComponentInstanceOfType(Class componentType) { + final ComponentAdapter componentAdapter = getComponentAdapterOfType(componentType); + return componentAdapter == null ? null : getInstance(componentAdapter); + } + + @Nullable + private Object getInstance(@NotNull ComponentAdapter componentAdapter) { + if (getComponentAdapters().contains(componentAdapter)) { + return getLocalInstance(componentAdapter); + } + if (parent != null) { + return parent.getComponentInstance(componentAdapter.getComponentKey()); + } + + return null; + } + + private Object getLocalInstance(@NotNull ComponentAdapter componentAdapter) { + PicoException firstLevelException; + try { + return componentAdapter.getComponentInstance(this); + } + catch (PicoInitializationException | PicoIntrospectionException e) { + firstLevelException = e; + } + + if (parent != null) { + Object instance = parent.getComponentInstance(componentAdapter.getComponentKey()); + if (instance != null) { + return instance; + } + } + + throw firstLevelException; + } + + @Override + @Nullable + public ComponentAdapter unregisterComponentByInstance(@NotNull Object componentInstance) { + for (ComponentAdapter adapter : getComponentAdapters()) { + Object o = getInstance(adapter); + if (componentInstance.equals(o)) { + return unregisterComponent(adapter.getComponentKey()); + } + } + return null; + } + + @Override + public void verify() { + new VerifyingVisitor().traverse(this); + } + + @Override + public void start() { + throw new UnsupportedOperationException(); + } + + @Override + public void stop() { + throw new UnsupportedOperationException(); + } + + @Override + public void dispose() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public MutablePicoContainer makeChildContainer() { + DefaultPicoContainer pc = new DefaultPicoContainer(this); + addChildContainer(pc); + return pc; + } + + @Override + public boolean addChildContainer(@NotNull PicoContainer child) { + return children.add(child); + } + + @Override + public boolean removeChildContainer(@NotNull PicoContainer child) { + return children.remove(child); + } + + @Override + public void accept(PicoVisitor visitor) { + visitor.visitContainer(this); + + for (ComponentAdapter adapter : getComponentAdapters()) { + adapter.accept(visitor); + } + for (PicoContainer child : new SmartList<>(children)) { + child.accept(visitor); + } + } + + @Override + public ComponentAdapter registerComponentInstance(@NotNull Object component) { + return registerComponentInstance(component.getClass(), component); + } + + @Override + public ComponentAdapter registerComponentInstance(@NotNull Object componentKey, @NotNull Object componentInstance) { + return registerComponent(new InstanceComponentAdapter(componentKey, componentInstance, DEFAULT_LIFECYCLE_STRATEGY)); + } + + @Override + public ComponentAdapter registerComponentImplementation(@NotNull Class componentImplementation) { + return registerComponentImplementation(componentImplementation, componentImplementation); + } + + @Override + public ComponentAdapter registerComponentImplementation(@NotNull Object componentKey, @NotNull Class componentImplementation) { + return registerComponentImplementation(componentKey, componentImplementation, null); + } + + @Override + public ComponentAdapter registerComponentImplementation(@NotNull Object componentKey, @NotNull Class componentImplementation, Parameter[] parameters) { + ComponentAdapter componentAdapter = new CachingConstructorInjectionComponentAdapter(componentKey, componentImplementation, parameters, true); + return registerComponent(componentAdapter); + } + + @Override + public PicoContainer getParent() { + return parent; + } + + /** + * A linked hash set that's copied on write operations. + * @param <T> + */ + private static class LinkedHashSetWrapper<T> { + private final Object lock = new Object(); + private volatile Set<T> immutableSet; + private LinkedHashSet<T> synchronizedSet = new LinkedHashSet<>(); + + public void add(@NotNull T element) { + synchronized (lock) { + if (!synchronizedSet.contains(element)) { + copySyncSetIfExposedAsImmutable().add(element); + } + } + } + + private LinkedHashSet<T> copySyncSetIfExposedAsImmutable() { + if (immutableSet != null) { + immutableSet = null; + synchronizedSet = new LinkedHashSet<>(synchronizedSet); + } + return synchronizedSet; + } + + public void remove(@Nullable T element) { + synchronized (lock) { + copySyncSetIfExposedAsImmutable().remove(element); + } + } + + @NotNull + public Set<T> getImmutableSet() { + Set<T> res = immutableSet; + if (res == null) { + synchronized (lock) { + res = immutableSet; + if (res == null) { + // Expose the same set as immutable. It should be never modified again. Next add/remove operations will copy synchronizedSet + immutableSet = res = Collections.unmodifiableSet(synchronizedSet); + } + } + } + + return res; + } + } + + @Override + public String toString() { + return "DefaultPicoContainer" + (getParent() == null ? " (root)" : " (parent="+getParent()+")"); + } +}
\ No newline at end of file diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectOne.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectOne.java new file mode 100644 index 00000000..489e8f34 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectOne.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions.impl; + +/** + * @author Alexander Kireyev + */ +public class DependentObjectOne { + private final XMLTestBean[] myTestBeans; + + public DependentObjectOne(XMLTestBean[] testBeans) { + myTestBeans = testBeans; + } + + public XMLTestBean[] getTestBeans() { + return myTestBeans; + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectThree.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectThree.java new file mode 100644 index 00000000..f6ccabfb --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectThree.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions.impl; + +/** + * @author Alexander Kireyev + */ +public class DependentObjectThree { + private final DependentObjectOne myOne; + private final DependentObjectTwo myTwo; + private final XMLTestBean[] myTestBeans; + + public DependentObjectThree(DependentObjectOne one, XMLTestBean[] testBeans, DependentObjectTwo two) { + myOne = one; + myTestBeans = testBeans; + myTwo = two; + } + + public DependentObjectOne getOne() { + return myOne; + } + + public XMLTestBean[] getTestBeans() { + return myTestBeans; + } + + public DependentObjectTwo getTwo() { + return myTwo; + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectTwo.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectTwo.java new file mode 100644 index 00000000..b337a5e8 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/DependentObjectTwo.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions.impl; + +/** + * @author Alexander Kireyev + */ +public class DependentObjectTwo { + private final DependentObjectOne myOne; + + public DependentObjectTwo(DependentObjectOne one) { + myOne = one; + } + + public DependentObjectOne getOne() { + return myOne; + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionComponentAdapterTest.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionComponentAdapterTest.java new file mode 100644 index 00000000..143fb898 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionComponentAdapterTest.java @@ -0,0 +1,43 @@ +// Copyright 2000-2018 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.extensions.impl; + +import com.intellij.openapi.extensions.DefaultPluginDescriptor; +import com.intellij.openapi.extensions.LoadingOrder; +import com.intellij.openapi.util.JDOMUtil; +import org.jdom.Element; +import org.jdom.JDOMException; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.picocontainer.defaults.DefaultPicoContainer; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +/** + * @author Alexander Kireyev + */ +public class ExtensionComponentAdapterTest { + @Test + public void testLoadingOrderReading() { + assertEquals(LoadingOrder.ANY, createAdapter(LoadingOrder.ANY).getOrder()); + assertEquals(LoadingOrder.FIRST, createAdapter(LoadingOrder.FIRST).getOrder()); + assertEquals(LoadingOrder.LAST, createAdapter(LoadingOrder.LAST).getOrder()); + assertEquals(LoadingOrder.before("test"), createAdapter(LoadingOrder.readOrder("BEFORE test")).getOrder()); + assertEquals(LoadingOrder.after("test"), createAdapter(LoadingOrder.readOrder("AFTER test")).getOrder()); + } + + @Test + public void testUnknownAttributes() throws IOException, JDOMException { + String name = TestExtensionClassOne.class.getName(); + Element element = JDOMUtil.load("<bean implementation=\"123\"/>"); + DefaultPicoContainer container = new DefaultPicoContainer(); + DefaultPluginDescriptor descriptor = new DefaultPluginDescriptor("test"); + new ExtensionComponentAdapter(name, element, container, descriptor).getComponentInstance(container); + } + + @NotNull + private static ExtensionComponentAdapter createAdapter(@NotNull LoadingOrder order) { + return new ExtensionComponentAdapter(Object.class.getName(), null, null, null, order, null); + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionPointImplTest.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionPointImplTest.java new file mode 100644 index 00000000..f2363240 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionPointImplTest.java @@ -0,0 +1,282 @@ +// Copyright 2000-2018 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.extensions.impl; + +import com.intellij.openapi.extensions.*; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.util.SmartList; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; +import org.picocontainer.defaults.DefaultPicoContainer; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * @author AKireyev + */ +public class ExtensionPointImplTest { + @Test + public void testCreate() { + ExtensionPoint<Integer> extensionPoint = buildExtensionPoint(Integer.class); + assertThat(extensionPoint.getName()).isEqualTo(ExtensionsImplTest.EXTENSION_POINT_NAME_1); + assertThat(extensionPoint.getClassName()).isEqualTo(Integer.class.getName()); + } + + @Test + public void testUnregisterObject() { + ExtensionPoint<Integer> extensionPoint = buildExtensionPoint(Integer.class); + extensionPoint.registerExtension(new Integer(123)); + Object[] extensions = extensionPoint.getExtensions(); + assertThat(extensions).hasSize(1); + extensionPoint.unregisterExtension(new Integer(123)); + extensions = extensionPoint.getExtensions(); + assertThat(extensions).isEmpty(); + } + + @Test + public void testRegisterUnregisterExtension() { + final AreaInstance area = new AreaInstance() {}; + final ExtensionPoint<Object> extensionPoint = new ExtensionPointImpl<>( + "an.extension.point", Object.class.getName(), ExtensionPoint.Kind.INTERFACE, buildExtensionArea(), area, + new UndefinedPluginDescriptor()); + + final boolean[] flags = new boolean[2]; + Extension extension = new Extension() { + @Override + public void extensionAdded(@NotNull ExtensionPoint extensionPoint1) { + assertThat(extensionPoint1).isSameAs(extensionPoint); + assertThat(extensionPoint1.getArea()).isSameAs(area); + flags[0] = true; + } + + @Override + public void extensionRemoved(@NotNull ExtensionPoint extensionPoint1) { + assertThat(extensionPoint1).isSameAs(extensionPoint); + assertThat(extensionPoint1.getArea()).isSameAs(area); + flags[1] = true; + } + }; + + extensionPoint.registerExtension(extension); + assertThat(flags[0]).describedAs("Register call is missed").isTrue(); + assertThat(flags[1]).isFalse(); + + extensionPoint.unregisterExtension(extension); + assertThat(flags[1]).describedAs("Unregister call is missed").isTrue(); + } + + @Test + public void testRegisterObject() { + ExtensionPoint<Integer> extensionPoint = buildExtensionPoint(Integer.class); + extensionPoint.registerExtension(new Integer(123)); + Object[] extensions = extensionPoint.getExtensions(); + assertThat(extensions).describedAs("One extension").hasSize(1); + assertThat(extensions).isInstanceOf(Integer[].class); + assertThat(extensions[0]).isEqualTo(new Integer(123)); + } + + @Test + public void testRegistrationOrder() { + ExtensionPoint<Integer> extensionPoint = buildExtensionPoint(Integer.class); + extensionPoint.registerExtension(new Integer(123)); + extensionPoint.registerExtension(new Integer(321), LoadingOrder.FIRST); + Object[] extensions = extensionPoint.getExtensions(); + assertThat(extensions).hasSize(2); + assertThat(extensions[0]).isEqualTo(new Integer(321)); + } + + @Test + public void testListener() { + ExtensionPoint<Integer> extensionPoint = buildExtensionPoint(Integer.class); + final boolean[] added = new boolean[1]; + final boolean[] removed = new boolean[1]; + extensionPoint.addExtensionPointListener(new ExtensionPointListener<Integer>() { + @Override + public void extensionAdded(@NotNull Integer extension, final PluginDescriptor pluginDescriptor) { + added[0] = true; + } + + @Override + public void extensionRemoved(@NotNull Integer extension, final PluginDescriptor pluginDescriptor) { + removed[0] = true; + } + }); + assertThat(added[0]).isFalse(); + assertThat(removed[0]).isFalse(); + extensionPoint.registerExtension(new Integer(123)); + assertThat(added[0]).isTrue(); + assertThat(removed[0]).isFalse(); + added[0] = false; + extensionPoint.unregisterExtension(new Integer(123)); + assertThat(added[0]).isFalse(); + assertThat(removed[0]).isTrue(); + } + + @Test + public void testLateListener() { + ExtensionPoint<Integer> extensionPoint = buildExtensionPoint(Integer.class); + final boolean[] added = new boolean[1]; + extensionPoint.registerExtension(new Integer(123)); + assertThat(added[0]).isFalse(); + extensionPoint.addExtensionPointListener(new ExtensionPointListener<Integer>() { + @Override + public void extensionAdded(@NotNull Integer extension, final PluginDescriptor pluginDescriptor) { + added[0] = true; + } + + @Override + public void extensionRemoved(@NotNull Integer extension, final PluginDescriptor pluginDescriptor) { + } + }); + assertThat(added[0]).isTrue(); + } + + @Test + @SuppressWarnings("unchecked") + public void testIncompatibleExtension() { + ExtensionPoint extensionPoint = buildExtensionPoint(Integer.class); + + try { + extensionPoint.registerExtension(new Double(0)); + fail("must throw"); + } + catch (AssertionError ignored) { + } + + assertThat(extensionPoint.getExtensions()).isEmpty(); + + extensionPoint.registerExtension(new Integer(0)); + assertThat(extensionPoint.getExtensions()).hasSize(1); + } + + @Test + public void testIncompatibleAdapter() { + ExtensionPointImpl<Integer> extensionPoint = buildExtensionPoint(Integer.class); + + extensionPoint.registerExtensionAdapter(stringAdapter()); + + try { + assertThat(extensionPoint.getExtensions()).isEmpty(); + fail("must throw"); + } + catch (AssertionError ignored) { + } + } + + @Test + public void testCompatibleAdapter() { + ExtensionPointImpl<Integer> extensionPoint = buildExtensionPoint(Integer.class); + extensionPoint.registerExtension(new Integer(0)); + assertThat(extensionPoint.getExtensions()).hasSize(1); + } + + @Test + public void testCancelledRegistration() { + ExtensionPoint<String> extensionPoint = buildExtensionPoint(String.class); + MyShootingComponentAdapter adapter = stringAdapter(); + + extensionPoint.registerExtension("first"); + assertThat(extensionPoint.getExtensions()).hasSize(1); + + extensionPoint.registerExtension("second", LoadingOrder.FIRST); // registers a wrapping adapter + ((ExtensionPointImpl)extensionPoint).registerExtensionAdapter(adapter); + adapter.setFire(true); + try { + extensionPoint.getExtensions(); + fail("PCE expected"); + } + catch (ProcessCanceledException ignored) { } + + adapter.setFire(false); + String[] extensions = extensionPoint.getExtensions(); + assertThat(extensions[0]).isEqualTo("second"); + assertThat(new SmartList<>(extensions[1])).containsAnyOf("", "first"); + assertThat(new SmartList<>(extensions[2])).containsAnyOf("", "first"); + assertThat(extensions[2]).isNotEqualTo(extensions[1]); + } + + @Test + public void testListenerNotifications() { + ExtensionPoint<String> extensionPoint = buildExtensionPoint(String.class); + final List<String> extensions = ContainerUtil.newArrayList(); + extensionPoint.addExtensionPointListener(new ExtensionPointListener<String>() { + @Override + public void extensionAdded(@NotNull String extension, @Nullable PluginDescriptor pluginDescriptor) { + extensions.add(extension); + } + }); + MyShootingComponentAdapter adapter = stringAdapter(); + + extensionPoint.registerExtension("first"); + assertThat(extensions).contains("first"); + + extensionPoint.registerExtension("second", LoadingOrder.FIRST); + ((ExtensionPointImpl)extensionPoint).registerExtensionAdapter(adapter); + adapter.setFire(true); + try { + extensionPoint.getExtensions(); + fail("PCE expected"); + } + catch (ProcessCanceledException ignored) { } + assertThat(extensions).contains("first", "second"); + + adapter.setFire(false); + extensionPoint.getExtensions(); + assertThat(extensions).contains("first", "second", ""); + } + + @Test + public void clientsCannotModifyCachedExtensions() { + ExtensionPoint<Integer> extensionPoint = buildExtensionPoint(Integer.class); + extensionPoint.registerExtension(4); + extensionPoint.registerExtension(2); + + Integer[] extensions = extensionPoint.getExtensions(); + assertThat(extensions).containsExactly(4, 2); + Arrays.sort(extensions); + assertThat(extensions).containsExactly(2, 4); + + assertThat(extensionPoint.getExtensions()).containsExactly(4, 2); + } + + private static <T> ExtensionPointImpl<T> buildExtensionPoint(Class<T> aClass) { + return new ExtensionPointImpl<>( + ExtensionsImplTest.EXTENSION_POINT_NAME_1, aClass.getName(), ExtensionPoint.Kind.INTERFACE, + buildExtensionArea(), null, new UndefinedPluginDescriptor()); + } + + private static ExtensionsAreaImpl buildExtensionArea() { + return new ExtensionsAreaImpl(new DefaultPicoContainer()); + } + + private static MyShootingComponentAdapter stringAdapter() { + return new MyShootingComponentAdapter(String.class.getName()); + } + + private static class MyShootingComponentAdapter extends ExtensionComponentAdapter { + private boolean myFire; + + MyShootingComponentAdapter(@NotNull String implementationClass) { + super(implementationClass, null, new DefaultPicoContainer(), new DefaultPluginDescriptor("test")); + } + + public void setFire(boolean fire) { + myFire = fire; + } + + @Override + public Object getExtension() { + if (myFire) { + throw new ProcessCanceledException(); + } + else { + return super.getExtension(); + } + } + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsAreaTest.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsAreaTest.java new file mode 100644 index 00000000..c23a9580 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsAreaTest.java @@ -0,0 +1,39 @@ +/* + * 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 com.intellij.openapi.extensions.impl; + +import org.junit.Test; +import org.picocontainer.MutablePicoContainer; +import org.picocontainer.defaults.DefaultPicoContainer; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author mike + */ +public class ExtensionsAreaTest { + @Test + public void testGetComponentAdapterDoesntDuplicateAdapters() { + MutablePicoContainer picoContainer = + new ExtensionsAreaImpl("foo", null, new DefaultPicoContainer()).getPicoContainer(); + picoContainer.registerComponentImplementation("runnable", ExtensionsAreaTest.class); + + List adapters = picoContainer.getComponentAdaptersOfType(ExtensionsAreaTest.class); + assertEquals(1, adapters.size()); + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsComplexTest.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsComplexTest.java new file mode 100644 index 00000000..59ffe6c6 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsComplexTest.java @@ -0,0 +1,122 @@ +// Copyright 2000-2018 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.extensions.impl; + +import com.intellij.openapi.extensions.AreaInstance; +import com.intellij.openapi.extensions.ExtensionPoint; +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.util.JDOMUtil; +import org.jdom.Element; +import org.jdom.JDOMException; +import org.jetbrains.annotations.NonNls; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Alexander Kireyev + */ +public class ExtensionsComplexTest { + private static final String PLUGIN_NAME = "the.test.plugin"; + private static final String PLUGIN_NAME_2 = "another.test.plugin"; + + private List<MyAreaInstance> myAreasToDispose; + + @Before + public void setUp() { + myAreasToDispose = new ArrayList<>(); + Extensions.registerAreaClass("area", null); + Extensions.registerAreaClass("child_area", "area"); + } + + @After + public void tearDown() { + for (MyAreaInstance instance : myAreasToDispose) { + Extensions.disposeArea(instance); + } + myAreasToDispose = null; + for (ExtensionPoint extensionPoint : Extensions.getRootArea().getExtensionPoints()) { + if (extensionPoint.getName().startsWith(PLUGIN_NAME) || extensionPoint.getName().startsWith(PLUGIN_NAME_2)) { + Extensions.getRootArea().unregisterExtensionPoint(extensionPoint.getName()); + } + } + } + + @Test + public void testPluginInit() throws IOException, JDOMException { + initExtensionPoints( + "<extensionPoints>\n" + + " <extensionPoint name=\"extensionPoint\" beanClass=\"com.intellij.openapi.extensions.impl.XMLTestBean\" />\n" + + " <extensionPoint name=\"dependentOne\" beanClass=\"com.intellij.openapi.extensions.impl.DependentObjectOne\" />\n" + + "</extensionPoints>", null); + initExtensions( + " <extensions xmlns=\"the.test.plugin\">\n" + + " <extensionPoint>\n" + + " <prop1>321</prop1>\n" + + " </extensionPoint>\n" + + " <dependentOne/>\n" + + " </extensions>", null); + + assertTrue(Extensions.getRootArea().hasExtensionPoint("the.test.plugin.extensionPoint")); + assertEquals(1, Extensions.getExtensions("the.test.plugin.extensionPoint").length); + ExtensionPoint<XMLTestBean> ep = Extensions.getRootArea().getExtensionPoint("the.test.plugin.extensionPoint"); + XMLTestBean bean = ep.getExtension(); + assertNotNull(bean); + assertEquals(321, bean.getProp1()); + assertEquals("the.test.plugin", bean.getPluginId().getIdString()); + + DependentObjectOne dependentObjectOne = (DependentObjectOne)Extensions.getRootArea().getExtensionPoint("the.test.plugin.dependentOne").getExtension(); + assertNotNull(dependentObjectOne); + assertEquals(1, dependentObjectOne.getTestBeans().length); + + AreaInstance areaInstance = new MyAreaInstance(); + Extensions.instantiateArea("area", areaInstance, null); + + initExtensionPoints( + "<extensionPoints>\n" + + " <extensionPoint name=\"dependentTwo\" beanClass=\"com.intellij.openapi.extensions.impl.DependentObjectTwo\" area=\"area\"/>\n" + + " <extensionPoint name=\"extensionPoint4area\" beanClass=\"com.intellij.openapi.extensions.impl.XMLTestBean\" area=\"area\" />\n" + + "</extensionPoints>", areaInstance); + + initExtensions( + " <extensions xmlns=\"the.test.plugin\">\n" + + " <extensionPoint4area area=\"area\"/>\n" + + " <dependentTwo area=\"area\"/>\n" + + " </extensions>", areaInstance); + + ExtensionPoint extensionPoint = Extensions.getArea(areaInstance).getExtensionPoint("the.test.plugin.extensionPoint4area"); + assertNotNull(extensionPoint); + assertSame(areaInstance, extensionPoint.getArea()); + assertNotNull(extensionPoint.getExtension()); + + DependentObjectTwo dependentObjectTwo = (DependentObjectTwo)Extensions.getArea(areaInstance).getExtensionPoint("the.test.plugin.dependentTwo").getExtension(); + assertNotNull(dependentObjectTwo); + assertSame(dependentObjectOne, dependentObjectTwo.getOne()); + } + + private static void initExtensionPoints(@NonNls String data, AreaInstance instance) throws IOException, JDOMException { + final Element element = JDOMUtil.load(data); + for (final Object o : element.getChildren()) { + Element child = (Element)o; + ((ExtensionsAreaImpl)Extensions.getArea(instance)).registerExtensionPoint(ExtensionsComplexTest.PLUGIN_NAME, child); + } + } + + private static void initExtensions(@NonNls String data, AreaInstance instance) throws IOException, JDOMException { + final Element element = JDOMUtil.load(data); + for (final Element child : element.getChildren()) { + ((ExtensionsAreaImpl)Extensions.getArea(instance)).registerExtension(element.getNamespaceURI(), child); + } + } + + private class MyAreaInstance implements AreaInstance { + private MyAreaInstance() { + myAreasToDispose.add(this); + } + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsImplTest.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsImplTest.java new file mode 100644 index 00000000..795133a5 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/ExtensionsImplTest.java @@ -0,0 +1,289 @@ +// Copyright 2000-2018 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.extensions.impl; + +import com.intellij.openapi.extensions.*; +import com.intellij.openapi.util.JDOMUtil; +import org.jdom.JDOMException; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.picocontainer.MutablePicoContainer; +import org.picocontainer.defaults.DefaultPicoContainer; + +import java.io.IOException; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * @author AKireyev + */ +public class ExtensionsImplTest { + public static final String EXTENSION_POINT_NAME_1 = "ext.point.one"; + + @Test + public void testCreateAndAccess() { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(null); + int numEP = extensionsArea.getExtensionPoints().length; + extensionsArea.registerExtensionPoint(EXTENSION_POINT_NAME_1, Integer.class.getName()); + assertEquals("Additional EP available", numEP + 1, extensionsArea.getExtensionPoints().length); + assertNotNull("EP by name available", extensionsArea.getExtensionPoint(EXTENSION_POINT_NAME_1)); + } + + @Test(expected = RuntimeException.class) + public void testInvalidActions() { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(null); + extensionsArea.registerExtensionPoint(EXTENSION_POINT_NAME_1, Integer.class.getName()); + extensionsArea.registerExtensionPoint(EXTENSION_POINT_NAME_1, Boolean.class.getName()); + fail("Should not allow duplicate registration"); + } + + @Test + public void testUnregisterEP() { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(null); + int numEP = extensionsArea.getExtensionPoints().length; + extensionsArea.registerExtensionPoint(EXTENSION_POINT_NAME_1, Integer.class.getName()); + + final boolean[] removed = {false}; + extensionsArea.getExtensionPoint(EXTENSION_POINT_NAME_1).addExtensionPointListener(new ExtensionPointListener<Object>() { + @Override + public void extensionRemoved(@NotNull Object extension, PluginDescriptor pluginDescriptor) { + removed[0] = true; + } + }); + extensionsArea.getExtensionPoint(EXTENSION_POINT_NAME_1).registerExtension(new Integer(123)); + extensionsArea.unregisterExtensionPoint(EXTENSION_POINT_NAME_1); + assertEquals("Extension point should be removed", numEP, extensionsArea.getExtensionPoints().length); + assertTrue("Extension point disposed", removed[0]); + } + + @Test + public void testAvailabilityListener() { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(null); + MyListener.reset(); + extensionsArea.getExtensionPoint(EPAvailabilityListenerExtension.EXTENSION_POINT_NAME).registerExtension( + new EPAvailabilityListenerExtension(EXTENSION_POINT_NAME_1, MyListener.class.getName())); + assertEquals(0, MyListener.regCount); + assertEquals(0, MyListener.remCount); + extensionsArea.registerExtensionPoint(EXTENSION_POINT_NAME_1, Integer.class.getName()); + assertEquals(1, MyListener.regCount); + assertEquals(0, MyListener.remCount); + MyListener.reset(); + extensionsArea.unregisterExtensionPoint(EXTENSION_POINT_NAME_1); + assertEquals(1, MyListener.remCount); + assertEquals(0, MyListener.regCount); + } + + @Test + public void testAvailability2Listeners() { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(null); + MyListener.reset(); + extensionsArea.registerExtensionPoint(EXTENSION_POINT_NAME_1, Integer.class.getName()); + extensionsArea.getExtensionPoint(EPAvailabilityListenerExtension.EXTENSION_POINT_NAME).registerExtension( + new EPAvailabilityListenerExtension(EXTENSION_POINT_NAME_1, MyListener.class.getName())); + extensionsArea.getExtensionPoint(EPAvailabilityListenerExtension.EXTENSION_POINT_NAME).registerExtension( + new EPAvailabilityListenerExtension(EXTENSION_POINT_NAME_1, MyListener.class.getName())); + assertEquals(2, MyListener.regCount); + assertEquals(0, MyListener.remCount); + MyListener.reset(); + extensionsArea.unregisterExtensionPoint(EXTENSION_POINT_NAME_1); + assertEquals(2, MyListener.remCount); + assertEquals(0, MyListener.regCount); + } + + @Test + public void testAvailabilityListenerAfter() { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(null); + extensionsArea.registerExtensionPoint(EXTENSION_POINT_NAME_1, Integer.class.getName()); + MyListener.reset(); + extensionsArea.getExtensionPoint(EPAvailabilityListenerExtension.EXTENSION_POINT_NAME).registerExtension( + new EPAvailabilityListenerExtension(EXTENSION_POINT_NAME_1, MyListener.class.getName())); + assertEquals(1, MyListener.regCount); + assertEquals(0, MyListener.remCount); + } + + @Test + public void testTryPicoContainer() { + DefaultPicoContainer rootContainer = new DefaultPicoContainer(); + rootContainer.registerComponentInstance("plugin1", new DefaultPicoContainer(rootContainer)); + rootContainer.registerComponentInstance("plugin2", new DefaultPicoContainer(rootContainer)); + MutablePicoContainer container1 = (MutablePicoContainer)rootContainer.getComponentInstance("plugin1"); + MutablePicoContainer container2 = (MutablePicoContainer)rootContainer.getComponentInstance("plugin2"); + container1.registerComponentImplementation("component1", MyComponent1.class); + container1.registerComponentImplementation("component1.1", MyComponent1.class); + container2.registerComponentImplementation("component2", MyComponent2.class); + MyInterface1 testInstance = () -> { }; + rootContainer.registerComponentInstance(testInstance); + MyComponent1 component1 = (MyComponent1)container1.getComponentInstance("component1"); + assertEquals(testInstance, component1.testObject); + rootContainer.registerComponentInstance("component1", component1); + MyComponent1 component11 = (MyComponent1)container1.getComponentInstance("component1.1"); + rootContainer.registerComponentInstance("component11", component11); + MyComponent2 component2 = (MyComponent2)container2.getComponentInstance("component2"); + assertEquals(testInstance, component2.testObject); + assertTrue(Arrays.asList(component2.comp1).contains(component1)); + assertTrue(Arrays.asList(component2.comp1).contains(component11)); + rootContainer.registerComponentInstance("component2", component2); + rootContainer.registerComponentImplementation(MyTestComponent.class); + MyTestComponent testComponent = (MyTestComponent)rootContainer.getComponentInstance(MyTestComponent.class); + assertTrue(Arrays.asList(testComponent.comp1).contains(component1)); + assertTrue(Arrays.asList(testComponent.comp1).contains(component11)); + assertEquals(component2, testComponent.comp2); + } + + @Test + public void testTryPicoContainer2() { + DefaultPicoContainer rootContainer = new DefaultPicoContainer(); + rootContainer.registerComponentImplementation("component1", MyComponent1.class); + rootContainer.registerComponentImplementation("component1.1", MyComponent1.class); + rootContainer.registerComponentImplementation("component2", MyComponent2.class); + rootContainer.registerComponentImplementation(MyTestComponent.class); + MyInterface1 testInstance = () -> { }; + rootContainer.registerComponentInstance(testInstance); + MyTestComponent testComponent = (MyTestComponent)rootContainer.getComponentInstance(MyTestComponent.class); + MyComponent2 component2 = (MyComponent2)rootContainer.getComponentInstance("component2"); + MyComponent1 component11 = (MyComponent1)rootContainer.getComponentInstance("component1.1"); + MyComponent1 component1 = (MyComponent1)rootContainer.getComponentInstance("component1"); + assertEquals(testInstance, component1.testObject); + assertEquals(testInstance, component2.testObject); + assertTrue(Arrays.asList(component2.comp1).contains(component1)); + assertTrue(Arrays.asList(component2.comp1).contains(component11)); + assertTrue(Arrays.asList(testComponent.comp1).contains(component1)); + assertTrue(Arrays.asList(testComponent.comp1).contains(component11)); + assertEquals(component2, testComponent.comp2); + } + + @Test + public void testExtensionsNamespaces() throws IOException, JDOMException { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(new DefaultPicoContainer()); + extensionsArea.registerExtensionPoint("plugin.ep1", TestExtensionClassOne.class.getName(), ExtensionPoint.Kind.BEAN_CLASS); + extensionsArea.registerExtension("plugin", JDOMUtil.load( + "<plugin:ep1 xmlns:plugin=\"plugin\" order=\"LAST\"><text>3</text></plugin:ep1>")); + extensionsArea.registerExtension("plugin", JDOMUtil.load( + "<ep1 xmlns=\"plugin\" order=\"FIRST\"><text>1</text></ep1>")); + extensionsArea.registerExtension("plugin", JDOMUtil.load( + "<extension point=\"plugin.ep1\"><text>2</text></extension>")); + ExtensionPoint extensionPoint = extensionsArea.getExtensionPoint("plugin.ep1"); + TestExtensionClassOne[] extensions = (TestExtensionClassOne[]) extensionPoint.getExtensions(); + assertEquals(3, extensions.length); + assertEquals("1", extensions[0].getText()); + assertEquals("2", extensions[1].getText()); + assertEquals("3", extensions[2].getText()); + } + + @Test + public void testExtensionsWithOrdering() throws IOException, JDOMException { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(new DefaultPicoContainer()); + extensionsArea.registerExtensionPoint("ep1", TestExtensionClassOne.class.getName(), ExtensionPoint.Kind.BEAN_CLASS); + extensionsArea.registerExtension("", JDOMUtil.load( + "<extension point=\"ep1\" order=\"LAST\"><text>3</text></extension>")); + extensionsArea.registerExtension("", JDOMUtil.load( + "<extension point=\"ep1\" order=\"FIRST\"><text>1</text></extension>")); + extensionsArea.registerExtension("", JDOMUtil.load( + "<extension point=\"ep1\"><text>2</text></extension>")); + ExtensionPoint extensionPoint = extensionsArea.getExtensionPoint("ep1"); + TestExtensionClassOne[] extensions = (TestExtensionClassOne[]) extensionPoint.getExtensions(); + assertEquals(3, extensions.length); + assertEquals("1", extensions[0].getText()); + assertEquals("2", extensions[1].getText()); + assertEquals("3", extensions[2].getText()); + } + + @Test + public void testExtensionsWithOrderingUpdate() throws IOException, JDOMException { + ExtensionsAreaImpl extensionsArea = new ExtensionsAreaImpl(new DefaultPicoContainer()); + extensionsArea.registerExtensionPoint("ep1", TestExtensionClassOne.class.getName(), ExtensionPoint.Kind.BEAN_CLASS); + extensionsArea.registerExtension("", JDOMUtil.load("<extension point=\"ep1\" id=\"_7\" order=\"LAST\"><text>7</text></extension>")); + extensionsArea.registerExtension("", JDOMUtil.load("<extension point=\"ep1\" id=\"fst\" order=\"FIRST\"><text>1</text></extension>")); + extensionsArea.registerExtension("", JDOMUtil.load("<extension point=\"ep1\" id=\"id\"><text>3</text></extension>")); + ExtensionPoint<TestExtensionClassOne> extensionPoint = extensionsArea.getExtensionPoint("ep1"); + TestExtensionClassOne[] extensions = extensionPoint.getExtensions(); + assertEquals(3, extensions.length); + assertEquals("1", extensions[0].getText()); + assertEquals("3", extensions[1].getText()); + assertEquals("7", extensions[2].getText()); + TestExtensionClassOne extension = new TestExtensionClassOne("xxx"); + extensionPoint.registerExtension(extension); + extensionPoint.unregisterExtension(extension); + extensionsArea.registerExtension("", JDOMUtil.load("<extension point=\"ep1\" order=\"BEFORE id\"><text>2</text></extension>")); + extensionsArea.registerExtension("", JDOMUtil.load("<extension point=\"ep1\" order=\"AFTER id\"><text>4</text></extension>")); + extensionsArea.registerExtension("", JDOMUtil.load("<extension point=\"ep1\" order=\"last, after _7\"><text>8</text></extension>")); + extensionsArea.registerExtension("", JDOMUtil.load("<extension point=\"ep1\" order=\"after:id, before _7, after fst\"><text>5</text></extension>")); + extensionPoint.registerExtension(new TestExtensionClassOne("6")); + extensions = extensionPoint.getExtensions(); + assertEquals(8, extensions.length); + assertEquals("1", extensions[0].getText()); + assertEquals("2", extensions[1].getText()); + assertEquals("3", extensions[2].getText()); + assertTrue("4".equals(extensions[3].getText()) || "5".equals(extensions[3].getText()) ); + assertTrue("4".equals(extensions[4].getText()) || "5".equals(extensions[4].getText()) ); + assertEquals("6", extensions[5].getText()); + assertEquals("7", extensions[6].getText()); + assertEquals("8", extensions[7].getText()); + } + + public interface MyInterface1 extends Runnable { + } + + public interface MyInterface2 { + } + + public interface MyInterface3 extends Runnable { + } + + public interface MyInterface4 extends MyInterface1, MyInterface2, MyInterface3 { + } + + public static class MyClass implements MyInterface4 { + @Override + public void run() { + } + } + + public static class MyComponent1 { + public MyInterface1 testObject; + + public MyComponent1(MyInterface1 testObject) { + this.testObject = testObject; + } + } + + public static class MyComponent2 { + public MyInterface1 testObject; + public MyComponent1[] comp1; + + public MyComponent2(MyComponent1[] comp1, MyInterface1 testObject) { + this.comp1 = comp1; + this.testObject = testObject; + } + } + + public static class MyTestComponent { + public MyComponent1[] comp1; + public MyComponent2 comp2; + + public MyTestComponent(MyComponent1[] comp1, MyComponent2 comp2) { + this.comp1 = comp1; + this.comp2 = comp2; + } + } + + public static class MyListener implements ExtensionPointAvailabilityListener { + public static int regCount; + public static int remCount; + + @Override + public void extensionPointRegistered(@NotNull ExtensionPoint extensionPoint) { + regCount++; + } + + @Override + public void extensionPointRemoved(@NotNull ExtensionPoint extensionPoint) { + remCount++; + } + + public static void reset() { + regCount = 0; + remCount = 0; + } + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/LoadingOrderTest.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/LoadingOrderTest.java new file mode 100644 index 00000000..4bb62e68 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/LoadingOrderTest.java @@ -0,0 +1,190 @@ +// Copyright 2000-2018 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.extensions.impl; + +import com.intellij.openapi.extensions.LoadingOrder; +import com.intellij.openapi.extensions.SortingException; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Alexander Kireyev + */ +public class LoadingOrderTest { + @Test + public void testSimpleSorting() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + target.add(createElement(LoadingOrder.ANY, null, "Any")); + target.add(createElement(LoadingOrder.FIRST, null, "1")); + target.add(createElement(LoadingOrder.LAST, null, "2")); + target.add(createElement(LoadingOrder.ANY, null, "Any")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + assertSequence(array, "1AnyAny2"); + } + + @Test + public void testStability() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + target.add(createElement(LoadingOrder.ANY, null, "1")); + target.add(createElement(LoadingOrder.ANY, null, "2")); + target.add(createElement(LoadingOrder.ANY, null, "3")); + target.add(createElement(LoadingOrder.ANY, null, "4")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + assertSequence(array, "1234"); + } + + @Test + public void testComplexSorting() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + String idOne = "idOne"; + String idTwo = "idTwo"; + target.add(createElement(LoadingOrder.before(idTwo), idOne, "2")); + target.add(createElement(LoadingOrder.FIRST, null, "0")); + target.add(createElement(LoadingOrder.LAST, null, "5")); + target.add(createElement(LoadingOrder.after(idTwo), null, "4")); + target.add(createElement(LoadingOrder.ANY, idTwo, "3")); + target.add(createElement(LoadingOrder.before(idOne), null, "1")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + assertSequence(array, "012345"); + } + + @Test + public void testComplexSorting2() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + String idOne = "idOne"; + target.add(createElement(LoadingOrder.before(idOne), null, "2")); + target.add(createElement(LoadingOrder.after(idOne), null, "4")); + target.add(createElement(LoadingOrder.FIRST, null, "1")); + target.add(createElement(LoadingOrder.ANY, idOne, "3")); + target.add(createElement(LoadingOrder.ANY, null, "5")); + target.add(createElement(LoadingOrder.LAST, null, "6")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + assertSequence(array, "123456"); + } + + @Test + public void testComplexSortingBeforeLast() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + target.add(createElement(LoadingOrder.LAST, "1", "1")); + target.add(createElement(LoadingOrder.readOrder("last,before 1"), null, "2")); + target.add(createElement(LoadingOrder.ANY, null, "3")); + target.add(createElement(LoadingOrder.before("1'"), null, "4")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + assertSequence(array, "3421"); + } + + private static void assertSequence(LoadingOrder.Orderable[] array, String expected) { + LoadingOrder.sort(array); + StringBuffer sequence = buildSequence(array); + assertEquals(expected, sequence.toString()); + } + + private static StringBuffer buildSequence(LoadingOrder.Orderable[] array) { + StringBuffer sequence = new StringBuffer(); + for (LoadingOrder.Orderable adapter : array) { + sequence.append(((MyOrderable)adapter).getID()); + } + return sequence; + } + + @Test + public void testFailingSortingBeforeFirst() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + target.add(createElement(LoadingOrder.ANY, null, "good")); + target.add(createElement(LoadingOrder.FIRST, "first", "bad")); + target.add(createElement(LoadingOrder.LAST, null, "good")); + target.add(createElement(LoadingOrder.before("first"), null, "bad")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + checkSortingFailure(array); + } + + @Test + public void testFailingSortingFirst() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + target.add(createElement(LoadingOrder.ANY, null, "2")); + target.add(createElement(LoadingOrder.FIRST, "first", "1")); + target.add(createElement(LoadingOrder.LAST, null, "3")); + target.add(createElement(LoadingOrder.FIRST, null, "1")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + assertSequence(array, "1123"); + } + + private static void checkSortingFailure(LoadingOrder.Orderable[] array) { + try { + LoadingOrder.sort(array); + fail("Should have failed"); + } + catch (SortingException e) { + LoadingOrder.Orderable[] conflictingElements = e.getConflictingElements(); + assertEquals(2, conflictingElements.length); + assertEquals("bad", ((MyOrderable)conflictingElements[0]).getID()); + assertEquals("bad", ((MyOrderable)conflictingElements[1]).getID()); + } + } + + @Test + public void testFailingSortingAfterLast() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + target.add(createElement(LoadingOrder.after("last"), null, "bad")); + target.add(createElement(LoadingOrder.FIRST, null, "good")); + target.add(createElement(LoadingOrder.LAST, "last", "bad")); + target.add(createElement(LoadingOrder.ANY, null, "good")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + checkSortingFailure(array); + } + + @Test + public void testFailingSortingLast() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + target.add(createElement(LoadingOrder.LAST, null, "3")); + target.add(createElement(LoadingOrder.FIRST, null, "1")); + target.add(createElement(LoadingOrder.LAST, "last", "3")); + target.add(createElement(LoadingOrder.ANY, null, "2")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + assertSequence(array, "1233"); + } + + @Test + public void testFailingSortingComplex() { + List<LoadingOrder.Orderable> target = new ArrayList<>(); + target.add(createElement(LoadingOrder.after("2"), "1", "bad")); + target.add(createElement(LoadingOrder.after("3"), "2", "bad")); + target.add(createElement(LoadingOrder.after("1"), "3", "bad")); + LoadingOrder.Orderable[] array = target.toArray(new LoadingOrder.Orderable[0]); + checkSortingFailure(array); + } + + private static LoadingOrder.Orderable createElement(final LoadingOrder order, final String idString, final String elementId) { + return new MyOrderable(order, idString, elementId); + } + + private static class MyOrderable implements LoadingOrder.Orderable { + private final LoadingOrder myOrder; + private final String myOrderId; + private final String myId; + + MyOrderable(LoadingOrder order, String orderId, String id) { + myOrder = order; + myOrderId = orderId; + myId = id; + } + + @Override + public String getOrderId() { + return myOrderId; + } + + @Override + public LoadingOrder getOrder() { + return myOrder; + } + + public String getID() { + return myId; + } + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/NonCreatableClass.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/NonCreatableClass.java new file mode 100644 index 00000000..7094143b --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/NonCreatableClass.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions.impl; + +/** + * @author Alexander Kireyev + */ +public class NonCreatableClass { + static { + if (true) { + throw new RuntimeException("Cannot be created"); + } + } + + public NonCreatableClass() { + throw new RuntimeException("Cannot be created"); + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/TestExtensionClassOne.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/TestExtensionClassOne.java new file mode 100644 index 00000000..74e60447 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/TestExtensionClassOne.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions.impl; + +import com.intellij.util.xmlb.annotations.Tag; + +/** + * @author Alexander Kireyev + */ +public class TestExtensionClassOne { + @Tag("text") + public String myText; + + public TestExtensionClassOne() { + } + + public TestExtensionClassOne(String text) { + myText = text; + } + + public String getText() { + return myText; + } +} diff --git a/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/XMLTestBean.java b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/XMLTestBean.java new file mode 100644 index 00000000..5c800877 --- /dev/null +++ b/platform/extensions/testSrc/com/intellij/openapi/extensions/impl/XMLTestBean.java @@ -0,0 +1,86 @@ +/* + * Copyright 2000-2009 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 com.intellij.openapi.extensions.impl; + +import com.intellij.openapi.extensions.PluginAware; +import com.intellij.openapi.extensions.PluginDescriptor; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.util.xmlb.annotations.Tag; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * @author Alexander Kireyev + */ +public class XMLTestBean implements PluginAware { + private boolean otherProperty; + private int prop1; + private Object prop2; + private Collection<String> collectionProperty = new ArrayList<>(); + private PluginId pluginId; + + public XMLTestBean() { + } + + public XMLTestBean(Collection aCollectionProperty, boolean aOtherProperty, int aProp1) { + collectionProperty = aCollectionProperty; + otherProperty = aOtherProperty; + prop1 = aProp1; + } + + public boolean isOtherProperty() { + return otherProperty; + } + + public void setOtherProperty(boolean otherProperty) { + this.otherProperty = otherProperty; + } + + @Tag("prop1") + public int getProp1() { + return prop1; + } + + public void setProp1(int prop1) { + this.prop1 = prop1; + } + + public Object getProp2() { + return prop2; + } + + public void setProp2(Object prop2) { + this.prop2 = prop2; + } + + public Collection<String> getCollectionProperty() { + return collectionProperty; + } + + public void setCollectionProperty(Collection<String> collectionProperty) { + this.collectionProperty = collectionProperty; + } + + @Override + public void setPluginDescriptor(PluginDescriptor pluginDescriptor) { + pluginId = pluginDescriptor.getPluginId(); + } + + public PluginId getPluginId() { + return pluginId; + } +} |