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