3257e14a2d825d2b75d851ba2963ded1ed8a49f8
[idea/community.git] / platform / util / src / com / intellij / util / EventDispatcher.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.util;
3
4 import com.intellij.openapi.Disposable;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.util.Disposer;
7 import com.intellij.openapi.util.Getter;
8 import com.intellij.openapi.util.StaticGetter;
9 import com.intellij.util.containers.ContainerUtil;
10 import com.intellij.util.containers.DisposableWrapperList;
11 import com.intellij.util.lang.CompoundRuntimeException;
12 import org.jetbrains.annotations.NonNls;
13 import org.jetbrains.annotations.NotNull;
14 import org.jetbrains.annotations.Nullable;
15 import org.jetbrains.annotations.TestOnly;
16
17 import java.lang.reflect.InvocationHandler;
18 import java.lang.reflect.InvocationTargetException;
19 import java.lang.reflect.Method;
20 import java.lang.reflect.Proxy;
21 import java.util.*;
22
23 public class EventDispatcher<T extends EventListener> {
24   private static final Logger LOG = Logger.getInstance(EventDispatcher.class);
25
26   private T myMulticaster;
27
28   private final DisposableWrapperList<T> myListeners = new DisposableWrapperList<>();
29   @NotNull private final Class<T> myListenerClass;
30   @Nullable private final Map<String, Object> myMethodReturnValues;
31
32   @NotNull
33   public static <T extends EventListener> EventDispatcher<T> create(@NotNull Class<T> listenerClass) {
34     return new EventDispatcher<>(listenerClass, null);
35   }
36
37   @NotNull
38   public static <T extends EventListener> EventDispatcher<T> create(@NotNull Class<T> listenerClass, @NotNull Map<String, Object> methodReturnValues) {
39     assertNonVoidMethodReturnValuesAreDeclared(methodReturnValues, listenerClass);
40     return new EventDispatcher<>(listenerClass, methodReturnValues);
41   }
42
43   private static void assertNonVoidMethodReturnValuesAreDeclared(@NotNull Map<String, Object> methodReturnValues,
44                                                                  @NotNull Class<?> listenerClass) {
45     List<Method> declared = new ArrayList<>(ReflectionUtil.getClassPublicMethods(listenerClass));
46     for (final Map.Entry<String, Object> entry : methodReturnValues.entrySet()) {
47       final String methodName = entry.getKey();
48       Method found = ContainerUtil.find(declared, m -> methodName.equals(m.getName()));
49       assert found != null : "Method " + methodName + " must be declared in " + listenerClass;
50       assert !found.getReturnType().equals(void.class) :
51         "Method " + methodName + " must be non-void if you want to specify what its proxy should return";
52       Object returnValue = entry.getValue();
53       assert ReflectionUtil.boxType(found.getReturnType()).isAssignableFrom(returnValue.getClass()) :
54         "You specified that method " +
55         methodName + " proxy will return " + returnValue +
56         " but its return type is " + found.getReturnType() + " which is incompatible with " + returnValue.getClass();
57       declared.remove(found);
58     }
59     for (Method method : declared) {
60       assert method.getReturnType().equals(void.class) :
61         "Method "+method+" returns "+method.getReturnType()+" and yet you didn't specify what its proxy should return";
62     }
63   }
64
65   private EventDispatcher(@NotNull Class<T> listenerClass, @Nullable Map<String, Object> methodReturnValues) {
66     myListenerClass = listenerClass;
67     myMethodReturnValues = methodReturnValues;
68   }
69
70   public static <T> T createMulticaster(@NotNull Class<T> listenerClass,
71                                         @NotNull Getter<? extends Iterable<T>> listeners) {
72     return createMulticaster(listenerClass, null, listeners);
73   }
74
75   @NotNull
76   static <T> T createMulticaster(@NotNull Class<T> listenerClass,
77                                  @Nullable Map<String, Object> methodReturnValues,
78                                  @NotNull Getter<? extends Iterable<T>> listeners) {
79     LOG.assertTrue(listenerClass.isInterface(), "listenerClass must be an interface");
80     InvocationHandler handler = new InvocationHandler() {
81       @Override
82       @NonNls
83       public Object invoke(Object proxy, final Method method, final Object[] args) {
84         @NonNls String methodName = method.getName();
85         if (method.getDeclaringClass().getName().equals("java.lang.Object")) {
86           return handleObjectMethod(proxy, args, methodName);
87         }
88         else if (methodReturnValues != null && methodReturnValues.containsKey(methodName)) {
89           return methodReturnValues.get(methodName);
90         }
91         else {
92           dispatchVoidMethod(listeners.get(), method, args);
93           return null;
94         }
95       }
96     };
97
98     //noinspection unchecked
99     return (T)Proxy.newProxyInstance(listenerClass.getClassLoader(), new Class[]{listenerClass}, handler);
100   }
101
102   @Nullable
103   public static Object handleObjectMethod(Object proxy, Object[] args, String methodName) {
104     if (methodName.equals("toString")) {
105       return "Multicaster";
106     }
107     else if (methodName.equals("hashCode")) {
108       return System.identityHashCode(proxy);
109     }
110     else if (methodName.equals("equals")) {
111       return proxy == args[0] ? Boolean.TRUE : Boolean.FALSE;
112     }
113     else {
114       LOG.error("Incorrect Object's method invoked for proxy:" + methodName);
115       return null;
116     }
117   }
118
119   @NotNull
120   public T getMulticaster() {
121     T multicaster = myMulticaster;
122     if (multicaster == null) {
123       // benign race
124       myMulticaster = multicaster = createMulticaster(myListenerClass, myMethodReturnValues, new StaticGetter<Iterable<T>>(myListeners));
125     }
126     return multicaster;
127   }
128
129   private static <T> void dispatchVoidMethod(@NotNull Iterable<? extends T> listeners, @NotNull Method method, Object[] args) {
130     List<Throwable> exceptions = null;
131     method.setAccessible(true);
132
133     for (T listener : listeners) {
134       try {
135         method.invoke(listener, args);
136       }
137       catch (Throwable e) {
138         //noinspection InstanceofCatchParameter
139         Throwable cause = e instanceof InvocationTargetException && e.getCause() != null ? e.getCause() : e;
140         // Do nothing for AbstractMethodError. This listener just does not implement something newly added yet.
141         // AbstractMethodError is normally wrapped in InvocationTargetException,
142         // but some Java versions didn't do it in some cases (see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6531596)
143         if (cause instanceof AbstractMethodError) continue;
144         if (exceptions == null) exceptions = new SmartList<>();
145         exceptions.add(cause);
146       }
147     }
148     CompoundRuntimeException.throwIfNotEmpty(exceptions);
149   }
150
151   public void addListener(@NotNull T listener) {
152     myListeners.add(listener);
153   }
154
155   public void addListener(@NotNull T listener, @NotNull Disposable parentDisposable) {
156     myListeners.add(listener, parentDisposable);
157   }
158
159   public void removeListener(@NotNull T listener) {
160     myListeners.remove(listener);
161   }
162
163   public boolean hasListeners() {
164     return !myListeners.isEmpty();
165   }
166
167   @NotNull
168   public List<T> getListeners() {
169     return myListeners;
170   }
171
172   @TestOnly
173   public void neuterMultiCasterWhilePerformanceTestIsRunningUntil(@NotNull Disposable disposable) {
174     T multicaster = myMulticaster;
175     myMulticaster = createMulticaster(myListenerClass, myMethodReturnValues, new StaticGetter<Iterable<T>>(Collections.emptyList()));
176     Disposer.register(disposable, () -> myMulticaster = multicaster);
177   }
178 }