001/* 002 * Copyright (C) 2014 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the License 010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 011 * or implied. See the License for the specific language governing permissions and limitations under 012 * the License. 013 */ 014 015package org.openstreetmap.josm.eventbus; 016 017import java.lang.reflect.Method; 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Map.Entry; 028import java.util.Objects; 029import java.util.Set; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.ConcurrentMap; 032import java.util.concurrent.CopyOnWriteArraySet; 033import java.util.stream.Collectors; 034 035import org.openstreetmap.josm.tools.MultiMap; 036 037/** 038 * Registry of subscribers to a single event bus. 039 * 040 * @author Colin Decker 041 */ 042final class SubscriberRegistry { 043 044 /** 045 * All registered subscribers, indexed by event type. 046 * 047 * <p>The {@link CopyOnWriteArraySet} values make it easy and relatively lightweight to get an 048 * immutable snapshot of all current subscribers to an event without any locking. 049 */ 050 private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers = 051 new ConcurrentHashMap<>(); 052 053 /** The event bus this registry belongs to. */ 054 private final EventBus bus; 055 056 /** 057 * Constructs a new {@code SubscriberRegistry}. 058 * @param bus event bus 059 */ 060 SubscriberRegistry(EventBus bus) { 061 this.bus = Objects.requireNonNull(bus); 062 } 063 064 /** 065 * Registers all subscriber methods on the given listener object. 066 * @param listener listener 067 */ 068 void register(Object listener) { 069 MultiMap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener); 070 071 for (Entry<Class<?>, Set<Subscriber>> entry : listenerMethods.entrySet()) { 072 Class<?> eventType = entry.getKey(); 073 Collection<Subscriber> eventMethodsInListener = entry.getValue(); 074 075 CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType); 076 077 if (eventSubscribers == null) { 078 CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet<>(); 079 eventSubscribers = 080 firstNonNull(subscribers.putIfAbsent(eventType, newSet), newSet); 081 } 082 083 eventSubscribers.addAll(eventMethodsInListener); 084 } 085 } 086 087 /** Unregisters all subscribers on the given listener object. 088 * @param listener listener 089 */ 090 void unregister(Object listener) { 091 MultiMap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener); 092 093 for (Entry<Class<?>, Set<Subscriber>> entry : listenerMethods.entrySet()) { 094 Class<?> eventType = entry.getKey(); 095 Collection<Subscriber> listenerMethodsForType = entry.getValue(); 096 097 CopyOnWriteArraySet<Subscriber> currentSubscribers = subscribers.get(eventType); 098 if (currentSubscribers == null || !currentSubscribers.removeAll(listenerMethodsForType)) { 099 // if removeAll returns true, all we really know is that at least one subscriber was 100 // removed... however, barring something very strange we can assume that if at least one 101 // subscriber was removed, all subscribers on listener for that event type were... after 102 // all, the definition of subscribers on a particular class is totally static 103 throw new IllegalArgumentException( 104 "missing event subscriber for an annotated method. Is " + listener + " registered?"); 105 } 106 107 // don't try to remove the set if it's empty; that can't be done safely without a lock 108 // anyway, if the set is empty it'll just be wrapping an array of length 0 109 } 110 } 111 112 /** 113 * Returns subscribers for given {@code eventType}. Only used for unit tests. 114 * @param eventType event type 115 * @return subscribers for given {@code eventType}. Can be empty, but never null 116 */ 117 Set<Subscriber> getSubscribersForTesting(Class<?> eventType) { 118 return firstNonNull(subscribers.get(eventType), new HashSet<Subscriber>()); 119 } 120 121 /** 122 * Gets an iterator representing an immutable snapshot of all subscribers to the given event at 123 * the time this method is called. 124 * @param event event 125 * @return subscribers iterator 126 */ 127 Iterator<Subscriber> getSubscribers(Object event) { 128 Set<Class<?>> eventTypes = flattenHierarchy(event.getClass()); 129 130 List<Subscriber> subscriberList = new ArrayList<>(eventTypes.size()); 131 132 for (Class<?> eventType : eventTypes) { 133 CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType); 134 if (eventSubscribers != null) { 135 // eager no-copy snapshot 136 subscriberList.addAll(eventSubscribers); 137 } 138 } 139 140 return Collections.unmodifiableList(subscriberList).iterator(); 141 } 142 143 /** 144 * Returns all subscribers for the given listener grouped by the type of event they subscribe to. 145 * @param listener listener 146 * @return all subscribers for the given listener 147 */ 148 private MultiMap<Class<?>, Subscriber> findAllSubscribers(Object listener) { 149 MultiMap<Class<?>, Subscriber> methodsInListener = new MultiMap<>(); 150 Class<?> clazz = listener.getClass(); 151 for (Method method : getAnnotatedMethods(clazz)) { 152 Class<?>[] parameterTypes = method.getParameterTypes(); 153 Class<?> eventType = parameterTypes[0]; 154 methodsInListener.put(eventType, Subscriber.create(bus, listener, method)); 155 } 156 return methodsInListener; 157 } 158 159 private static List<Method> getAnnotatedMethods(Class<?> clazz) { 160 return getAnnotatedMethodsNotCached(clazz); 161 } 162 163 private static List<Method> getAnnotatedMethodsNotCached(Class<?> clazz) { 164 Set<? extends Class<?>> supertypes = getClassesAndInterfaces(clazz); 165 Map<MethodIdentifier, Method> identifiers = new HashMap<>(); 166 for (Class<?> supertype : supertypes) { 167 for (Method method : supertype.getDeclaredMethods()) { 168 if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) { 169 // TODO(cgdecker): Should check for a generic parameter type and error out 170 Class<?>[] parameterTypes = method.getParameterTypes(); 171 if (parameterTypes.length != 1) { 172 throw new IllegalArgumentException(String.format( 173 "Method %s has @Subscribe annotation but has %s parameters." 174 + "Subscriber methods must have exactly 1 parameter.", 175 method, 176 parameterTypes.length)); 177 } 178 179 MethodIdentifier ident = new MethodIdentifier(method); 180 if (!identifiers.containsKey(ident)) { 181 identifiers.put(ident, method); 182 } 183 } 184 } 185 } 186 return new ArrayList<>(identifiers.values()); 187 } 188 189 /** Global cache of classes to their flattened hierarchy of supertypes. */ 190 private static final Map<Class<?>, Set<Class<?>>> flattenHierarchyCache = new HashMap<>(); 191 192 /** 193 * Flattens a class's type hierarchy into a set of {@code Class} objects including all 194 * superclasses (transitively) and all interfaces implemented by these superclasses. 195 * @param concreteClass concrete class 196 * @return set of {@code Class} objects including all superclasses and interfaces 197 */ 198 static Set<Class<?>> flattenHierarchy(Class<?> concreteClass) { 199 return flattenHierarchyCache.computeIfAbsent( 200 concreteClass, SubscriberRegistry::getClassesAndInterfaces); 201 } 202 203 private static final class MethodIdentifier { 204 205 private final String name; 206 private final List<Class<?>> parameterTypes; 207 208 MethodIdentifier(Method method) { 209 this.name = method.getName(); 210 this.parameterTypes = Arrays.asList(method.getParameterTypes()); 211 } 212 213 @Override 214 public int hashCode() { 215 return Objects.hash(name, parameterTypes); 216 } 217 218 @Override 219 public boolean equals(Object o) { 220 if (o instanceof MethodIdentifier) { 221 MethodIdentifier ident = (MethodIdentifier) o; 222 return name.equals(ident.name) && parameterTypes.equals(ident.parameterTypes); 223 } 224 return false; 225 } 226 } 227 228 /** 229 * Returns the first of two given parameters that is not {@code null}, if either is, or otherwise 230 * throws a {@link NullPointerException}. 231 * 232 * <p>To find the first non-null element in an iterable, use {@code Iterables.find(iterable, 233 * Predicates.notNull())}. For varargs, use {@code Iterables.find(Arrays.asList(a, b, c, ...), 234 * Predicates.notNull())}, static importing as necessary. 235 * 236 * @param <T> object type 237 * @param first first object 238 * @param second second object 239 * 240 * @return {@code first} if it is non-null; otherwise {@code second} if it is non-null 241 * @throws NullPointerException if both {@code first} and {@code second} are null 242 * @since 18.0 (since 3.0 as {@code Objects.firstNonNull()}). 243 */ 244 static <T> T firstNonNull(T first, T second) { 245 if (first != null) { 246 return first; 247 } 248 if (second != null) { 249 return second; 250 } 251 throw new NullPointerException("Both parameters are null"); 252 } 253 254 private static Set<Class<?>> getClassesAndInterfaces(Class<?> clazz) { 255 Set<Class<?>> result = new HashSet<>(); 256 Class<?> c = clazz; 257 while (c != null) { 258 result.add(c); 259 for (Set<Class<?>> interfaces : Arrays.stream(c.getInterfaces()).map( 260 SubscriberRegistry::getClassesAndInterfaces).collect(Collectors.toList())) { 261 result.addAll(interfaces); 262 } 263 c = c.getSuperclass(); 264 } 265 return result; 266 } 267}