/*
* 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.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.*;
import com.intellij.openapi.util.Disposer;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.StringInterner;
import org.jdom.Element;
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 java.lang.reflect.Array;
import java.util.*;
/**
* @author AKireyev
*/
public class ExtensionPointImpl<T> implements ExtensionPoint<T> {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.extensions.impl.ExtensionPointImpl");
private final LogProvider myLogger;
private final AreaInstance myArea;
private final String myName;
private final String myClassName;
private final Kind myKind;
private final List<T> myExtensions = new ArrayList<T>();
private volatile T[] myExtensionsCache;
private final ExtensionsAreaImpl myOwner;
private final PluginDescriptor myDescriptor;
private final Set<ExtensionComponentAdapter> myExtensionAdapters = new LinkedHashSet<ExtensionComponentAdapter>();
private final List<ExtensionPointListener<T>> myEPListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final List<ExtensionComponentAdapter> myLoadedAdapters = new ArrayList<ExtensionComponentAdapter>();
private Class<T> myExtensionClass;
private static final StringInterner INTERNER = new StringInterner();
public ExtensionPointImpl(@NotNull String name,
@NotNull String className,
@NotNull Kind kind,
@NotNull ExtensionsAreaImpl owner,
AreaInstance area,
@NotNull LogProvider logger,
@NotNull PluginDescriptor descriptor) {
synchronized (INTERNER) {
myName = INTERNER.intern(name);
}
myClassName = className;
myKind = kind;
myOwner = owner;
myArea = area;
myLogger = logger;
myDescriptor = descriptor;
}
@NotNull
@Override
public String getName() {
return myName;
}
@Override
public AreaInstance getArea() {
return myArea;
}
@NotNull
@Override
public String getBeanClassName() {
return myClassName;
}
@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) {
assert myExtensions.size() == myLoadedAdapters.size();
ObjectComponentAdapter adapter = new ObjectComponentAdapter(extension, order);
assertClass(extension.getClass());
if (LoadingOrder.ANY == order) {
int index = myLoadedAdapters.size();
if (index > 0) {
ExtensionComponentAdapter lastAdapter = myLoadedAdapters.get(index - 1);
if (lastAdapter.getOrder() == LoadingOrder.LAST) {
index--;
}
}
internalRegisterExtension(extension, adapter, index, true);
}
else {
myExtensionAdapters.add(adapter);
processAdapters();
}
clearCache();
}
private void internalRegisterExtension(@NotNull T extension, @NotNull ExtensionComponentAdapter adapter, int index, boolean runNotifications) {
if (myExtensions.contains(extension)) {
myLogger.error("Extension was already added: " + extension);
return;
}
myExtensions.add(index, extension);
myLoadedAdapters.add(index, adapter);
if (runNotifications) {
if (extension instanceof Extension) {
try {
((Extension)extension).extensionAdded(this);
}
catch (Throwable e) {
myLogger.error(e);
}
}
clearCache();
notifyListenersOnAdd(extension, adapter.getPluginDescriptor());
}
}
private void notifyListenersOnAdd(@NotNull T extension, final PluginDescriptor pluginDescriptor) {
for (ExtensionPointListener<T> listener : myEPListeners) {
try {
listener.extensionAdded(extension, pluginDescriptor);
}
catch (Throwable e) {
myLogger.error(e);
}
}
}
@Override
@NotNull
public T[] getExtensions() {
T[] result = myExtensionsCache;
if (result == null) {
synchronized (this) {
result = myExtensionsCache;
if (result == null) {
processAdapters();
Class<T> extensionClass = getExtensionClass();
@SuppressWarnings("unchecked") T[] a = (T[])Array.newInstance(extensionClass, myExtensions.size());
result = myExtensions.toArray(a);
for (int i = result.length - 1; i >= 0; i--) {
T t = result[i];
if (i > 0 && result[i] == result[i - 1]) {
LOG.error("Duplicate extension found: " + t + "; " +
" Result: " + Arrays.toString(result) + ";\n" +
" extensions: " + myExtensions + ";\n" +
" getExtensionClass(): " + extensionClass + ";\n" +
" size:" + myExtensions.size() + ";" + result.length);
}
if (!extensionClass.isAssignableFrom(t.getClass())) {
LOG.error("Extension '" + t.getClass() + "' must be an instance of '" + extensionClass + "'",
new ExtensionException(t.getClass()));
result = ArrayUtil.remove(result, i); // we assume that usually all extensions are OK
}
}
myExtensionsCache = result;
}
}
}
return result;
}
@Override
public boolean hasAnyExtensions() {
final T[] cache = myExtensionsCache;
if (cache != null) {
return cache.length > 0;
}
synchronized (this) {
return myExtensionAdapters.size() + myLoadedAdapters.size() > 0;
}
}
private void processAdapters() {
int totalSize = myExtensionAdapters.size() + myLoadedAdapters.size();
if (totalSize != 0) {
List<ExtensionComponentAdapter> allAdapters = new ArrayList<ExtensionComponentAdapter>(totalSize);
allAdapters.addAll(myExtensionAdapters);
allAdapters.addAll(myLoadedAdapters);
myExtensions.clear();
ExtensionComponentAdapter[] loadedAdapters = myLoadedAdapters.isEmpty()
? ExtensionComponentAdapter.EMPTY_ARRAY
: myLoadedAdapters.toArray(new ExtensionComponentAdapter[myLoadedAdapters.size()]);
myLoadedAdapters.clear();
ExtensionComponentAdapter[] adapters = allAdapters.toArray(new ExtensionComponentAdapter[myExtensionAdapters.size()]);
LoadingOrder.sort(adapters);
final List<T> extensions = new ArrayList<T>(adapters.length);
for (ExtensionComponentAdapter adapter : adapters) {
@SuppressWarnings("unchecked") T extension = (T)adapter.getExtension();
assertClass(extension.getClass());
extensions.add(extension);
}
for (int i = 0; i < extensions.size(); i++) {
T extension = extensions.get(i);
internalRegisterExtension(extension, adapters[i], myExtensions.size(), ArrayUtilRt.find(loadedAdapters, adapters[i]) == -1);
}
myExtensionAdapters.clear();
}
}
@Override
@Nullable
public T getExtension() {
T[] extensions = getExtensions();
if (extensions.length == 0) return null;
return extensions[0];
}
@Override
public synchronized boolean hasExtension(@NotNull T extension) {
processAdapters();
return myExtensions.contains(extension);
}
@Override
public synchronized void unregisterExtension(@NotNull final T extension) {
final int index = getExtensionIndex(extension);
final ExtensionComponentAdapter adapter = myLoadedAdapters.get(index);
myOwner.getMutablePicoContainer().unregisterComponent(adapter.getComponentKey());
final MutablePicoContainer[] pluginContainers = myOwner.getPluginContainers();
for (MutablePicoContainer pluginContainer : pluginContainers) {
pluginContainer.unregisterComponent(adapter.getComponentKey());
}
processAdapters();
internalUnregisterExtension(extension, null);
}
private int getExtensionIndex(@NotNull T extension) {
int i = myExtensions.indexOf(extension);
if (i == -1) {
throw new IllegalArgumentException("Extension to be removed not found: " + extension);
}
return i;
}
private void internalUnregisterExtension(@NotNull T extension, PluginDescriptor pluginDescriptor) {
int index = getExtensionIndex(extension);
myExtensions.remove(index);
myLoadedAdapters.remove(index);
clearCache();
notifyListenersOnRemove(extension, pluginDescriptor);
if (extension instanceof Extension) {
Extension o = (Extension)extension;
try {
o.extensionRemoved(this);
}
catch (Throwable e) {
myLogger.error(e);
}
}
}
private void notifyListenersOnRemove(@NotNull T extensionObject, PluginDescriptor pluginDescriptor) {
for (ExtensionPointListener<T> listener : myEPListeners) {
try {
listener.extensionRemoved(extensionObject, pluginDescriptor);
}
catch (Throwable e) {
myLogger.error(e);
}
}
}
@Override
public synchronized void addExtensionPointListener(@NotNull final ExtensionPointListener<T> listener,
@NotNull Disposable parentDisposable) {
addExtensionPointListener(listener);
Disposer.register(parentDisposable, new Disposable() {
@Override
public void dispose() {
removeExtensionPointListener(listener);
}
});
}
@Override
public synchronized void addExtensionPointListener(@NotNull ExtensionPointListener<T> listener) {
processAdapters();
if (myEPListeners.add(listener)) {
for (ExtensionComponentAdapter componentAdapter : myLoadedAdapters.toArray(new ExtensionComponentAdapter[myLoadedAdapters.size()])) {
try {
@SuppressWarnings("unchecked") T extension = (T)componentAdapter.getExtension();
listener.extensionAdded(extension, componentAdapter.getPluginDescriptor());
}
catch (Throwable e) {
myLogger.error(e);
}
}
}
}
@Override
public synchronized void removeExtensionPointListener(@NotNull ExtensionPointListener<T> listener) {
for (ExtensionComponentAdapter componentAdapter : myLoadedAdapters.toArray(new ExtensionComponentAdapter[myLoadedAdapters.size()])) {
try {
@SuppressWarnings("unchecked") T extension = (T)componentAdapter.getExtension();
listener.extensionRemoved(extension, componentAdapter.getPluginDescriptor());
}
catch (Throwable e) {
myLogger.error(e);
}
}
boolean success = myEPListeners.remove(listener);
assert success;
}
@Override
public synchronized void reset() {
myOwner.removeAllComponents(myExtensionAdapters);
myExtensionAdapters.clear();
for (T extension : getExtensions()) {
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 = pluginClassLoader == null
? (Class<T>)Class.forName(myClassName) : (Class<T>)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) {
myExtensionAdapters.add(adapter);
clearCache();
}
private void assertClass(@NotNull Class<?> extensionClass) {
Class<T> expectedClass = getExtensionClass();
assert expectedClass.isAssignableFrom(extensionClass) : "Expected: " + expectedClass + "; Actual: " + extensionClass;
}
private void clearCache() {
myExtensionsCache = null;
}
synchronized boolean unregisterComponentAdapter(@NotNull ExtensionComponentAdapter componentAdapter) {
try {
if (myExtensionAdapters.remove(componentAdapter)) {
return true;
}
if (myLoadedAdapters.contains(componentAdapter)) {
final Object componentKey = componentAdapter.getComponentKey();
myOwner.getMutablePicoContainer().unregisterComponent(componentKey);
final MutablePicoContainer[] pluginContainers = myOwner.getPluginContainers();
for (MutablePicoContainer pluginContainer : pluginContainers) {
pluginContainer.unregisterComponent(componentKey);
}
@SuppressWarnings("unchecked") T extension = (T)componentAdapter.getExtension();
internalUnregisterExtension(extension, componentAdapter.getPluginDescriptor());
return true;
}
return false;
}
finally {
clearCache();
}
}
@TestOnly
final synchronized void notifyAreaReplaced(final ExtensionsArea area) {
for (final ExtensionPointListener<T> listener : myEPListeners) {
if (listener instanceof ExtensionPointAndAreaListener) {
((ExtensionPointAndAreaListener)listener).areaReplaced(area);
}
}
}
private static class ObjectComponentAdapter extends ExtensionComponentAdapter {
private final Object myExtension;
private final LoadingOrder myLoadingOrder;
private ObjectComponentAdapter(@NotNull Object extension, @NotNull LoadingOrder loadingOrder) {
super(Object.class.getName(), null, null, null, false);
myExtension = extension;
myLoadingOrder = loadingOrder;
}
@Override
public Object getExtension() {
return myExtension;
}
@Override
public LoadingOrder getOrder() {
return myLoadingOrder;
}
@Override
@Nullable
public String getOrderId() {
return null;
}
@Override
@NonNls
public Element getDescribingElement() {
return new Element("RuntimeExtension: " + myExtension);
}
}
}