/* * Copyright 2013-2016 consulo.io * * 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 consulo.extensions; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.AreaInstance; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.UserDataHolder; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * @author VISTALL * @see com.intellij.util.EventDispatcher * @since 20.04.2015 */ public abstract class CompositeExtensionPointName<T> { public static final Logger LOGGER = Logger.getInstance(CompositeExtensionPointName.class); @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface BooleanBreakResult { boolean breakValue(); } @NotNull public static <E> CompositeExtensionPointName<E> modulePoint(@NotNull String epName, @NotNull Class<E> clazz) { return new CompositeExtensionPointNameWithArea<E>(epName, clazz) { @Override protected boolean validateArea(@Nullable AreaInstance areaInstance) { return areaInstance instanceof Module; } }; } @NotNull public static <E> CompositeExtensionPointName<E> projectPoint(@NotNull String epName, @NotNull Class<E> clazz) { return new CompositeExtensionPointNameWithArea<E>(epName, clazz) { @Override protected boolean validateArea(@Nullable AreaInstance areaInstance) { return areaInstance instanceof Project; } }; } @NotNull public static <E> CompositeExtensionPointName<E> applicationPoint(@NotNull String epName, @NotNull Class<E> clazz) { return new CompositeExtensionPointNameNoArea<E>(epName, clazz); } private static class MyInvocationHandler<T> implements InvocationHandler { private final String myName; private final T[] myExtensions; public MyInvocationHandler(String name, T[] extensions) { myName = name; myExtensions = extensions; } @Override @NonNls public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { if (method.getDeclaringClass().getName().equals("java.lang.Object")) { @NonNls String methodName = method.getName(); if (methodName.equals("toString")) { return "CompositeValue: " + myName; } else if (methodName.equals("hashCode")) { return Integer.valueOf(System.identityHashCode(proxy)); } else if (methodName.equals("equals")) { return proxy == args[0] ? Boolean.TRUE : Boolean.FALSE; } else { CompositeExtensionPointName.LOGGER.error("Incorrect Object's method invoked for proxy:" + methodName); return null; } } else { Class<?> returnType = method.getReturnType(); if (returnType == Void.TYPE) { return invokeVoidMethod(method, args); } else if (returnType == Boolean.TYPE) { return invokeBooleanMethod(method, args); } return invokeMethod(method, args); } } private Object invokeMethod(@NotNull Method method, Object[] args) { method.setAccessible(true); for (T listener : myExtensions) { try { Object value = method.invoke(listener, args); if (value != null) { return value; } } catch (AbstractMethodError ignored) { // Do nothing. This listener just does not implement something newly added yet. // AbstractMethodError is normally wrapped in InvocationTargetException, // but some Java versions didn't do it in some cases (see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6531596) } catch (RuntimeException e) { throw e; } catch (Exception e) { final Throwable cause = e.getCause(); if (cause instanceof RuntimeException) { throw (RuntimeException)cause; } else if (!(cause instanceof AbstractMethodError)) { // AbstractMethodError means this listener doesn't implement some new method in interface CompositeExtensionPointName.LOGGER.error(cause); } } } return null; } @NotNull private Boolean invokeBooleanMethod(@NotNull Method method, Object[] args) { method.setAccessible(true); Boolean breakValue = Boolean.TRUE; BooleanBreakResult annotation = method.getAnnotation(BooleanBreakResult.class); if (annotation != null) { breakValue = annotation.breakValue(); } for (T listener : myExtensions) { try { Boolean value = (Boolean)method.invoke(listener, args); if (breakValue.equals(value)) { return value; } } catch (AbstractMethodError ignored) { // Do nothing. This listener just does not implement something newly added yet. // AbstractMethodError is normally wrapped in InvocationTargetException, // but some Java versions didn't do it in some cases (see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6531596) } catch (RuntimeException e) { throw e; } catch (Exception e) { final Throwable cause = e.getCause(); if (cause instanceof RuntimeException) { throw (RuntimeException)cause; } else if (!(cause instanceof AbstractMethodError)) { // AbstractMethodError means this listener doesn't implement some new method in interface CompositeExtensionPointName.LOGGER.error(cause); } } } return !breakValue; } private Object invokeVoidMethod(@NotNull Method method, Object[] args) { method.setAccessible(true); for (T listener : myExtensions) { try { method.invoke(listener, args); } catch (AbstractMethodError ignored) { // Do nothing. This listener just does not implement something newly added yet. // AbstractMethodError is normally wrapped in InvocationTargetException, // but some Java versions didn't do it in some cases (see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6531596) } catch (RuntimeException e) { throw e; } catch (Exception e) { final Throwable cause = e.getCause(); if (cause instanceof RuntimeException) { throw (RuntimeException)cause; } else if (!(cause instanceof AbstractMethodError)) { // AbstractMethodError means this listener doesn't implement some new method in interface CompositeExtensionPointName.LOGGER.error(cause); } } } return null; } } private static class CompositeExtensionPointNameNoArea<E> extends CompositeExtensionPointName<E> { private volatile E myCompositeValue; protected CompositeExtensionPointNameNoArea(@NotNull String name, Class<E> clazz) { super(name, clazz); } @Nullable @Override protected E getCompositeValue(@Nullable AreaInstance areaInstance) { return myCompositeValue; } @Override protected void putCompositeValue(@Nullable AreaInstance areaInstance, E compositeValue) { myCompositeValue = compositeValue; } @Override protected boolean validateArea(@Nullable AreaInstance areaInstance) { return areaInstance == null; } } private static abstract class CompositeExtensionPointNameWithArea<E> extends CompositeExtensionPointName<E> { private Key<E> myCompositeValueKey; private CompositeExtensionPointNameWithArea(@NotNull String name, Class<E> clazz) { super(name, clazz); myCompositeValueKey = Key.create("CompositeExtensionPoint#" + name); } @Nullable @Override protected E getCompositeValue(@Nullable AreaInstance areaInstance) { assert areaInstance instanceof UserDataHolder; return ((UserDataHolder)areaInstance).getUserData(myCompositeValueKey); } @Override protected void putCompositeValue(@Nullable AreaInstance areaInstance, E compositeValue) { assert areaInstance instanceof UserDataHolder; ((UserDataHolder)areaInstance).putUserData(myCompositeValueKey, compositeValue); } } private final String myName; private final Class<T> myClazz; protected CompositeExtensionPointName(@NotNull String name, @NotNull Class<T> clazz) { myName = name; myClazz = clazz; } @NotNull private T buildCompositeValue(@Nullable AreaInstance areaInstance) { final T[] extensions = Extensions.getExtensions(myName, areaInstance); InvocationHandler handler = new MyInvocationHandler<T>(myName, extensions); //noinspection unchecked return (T)Proxy.newProxyInstance(myClazz.getClassLoader(), new Class[]{myClazz}, handler); } @Nullable protected abstract T getCompositeValue(@Nullable AreaInstance areaInstance); protected abstract void putCompositeValue(@Nullable AreaInstance areaInstance, T compositeValue); protected abstract boolean validateArea(@Nullable AreaInstance areaInstance); @NotNull public T composite() { return composite(null); } @NotNull public T composite(@Nullable AreaInstance areaInstance) { if (!validateArea(areaInstance)) { throw new IllegalArgumentException("Wrong area instance for '" + myName + "' extension point"); } T compositeValue = getCompositeValue(areaInstance); if (compositeValue != null) { return compositeValue; } compositeValue = buildCompositeValue(areaInstance); putCompositeValue(areaInstance, compositeValue); return compositeValue; } }