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}