All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.diffplug.common.eventbus.SubscriberRegistry Maven / Gradle / Ivy

There is a newer version: 1.2.0
Show newest version
/*
 * Original Guava code is copyright (C) 2015 The Guava Authors.
 * Modifications from Guava are copyright (C) 2016 DiffPlug.
 *
 * 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 com.diffplug.common.eventbus;

import static com.diffplug.common.base.Preconditions.checkArgument;
import static com.diffplug.common.base.Preconditions.checkNotNull;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.annotation.Nullable;

import com.google.j2objc.annotations.Weak;

import com.diffplug.common.annotations.VisibleForTesting;
import com.diffplug.common.base.MoreObjects;
import com.diffplug.common.base.Throwables;
import com.diffplug.common.cache.CacheBuilder;
import com.diffplug.common.cache.CacheLoader;
import com.diffplug.common.cache.LoadingCache;
import com.diffplug.common.collect.HashMultimap;
import com.diffplug.common.collect.ImmutableList;
import com.diffplug.common.collect.ImmutableSet;
import com.diffplug.common.collect.Iterators;
import com.diffplug.common.collect.Lists;
import com.diffplug.common.collect.Maps;
import com.diffplug.common.collect.Multimap;
import com.diffplug.common.reflect.TypeToken;
import com.diffplug.common.util.concurrent.UncheckedExecutionException;

/**
 * Registry of subscribers to a single event bus.
 *
 * @author Colin Decker
 */
final class SubscriberRegistry {

	/**
	 * All registered subscribers, indexed by event type.
	 *
	 * 

The {@link CopyOnWriteArraySet} values make it easy and relatively lightweight to get an * immutable snapshot of all current subscribers to an event without any locking. */ private final ConcurrentMap, CopyOnWriteArraySet> subscribers = Maps.newConcurrentMap(); /** * The event bus this registry belongs to. */ @Weak private final EventBus bus; SubscriberRegistry(EventBus bus) { this.bus = checkNotNull(bus); } /** * Registers all subscriber methods on the given listener object. */ void register(Object listener) { Multimap, Subscriber> listenerMethods = findAllSubscribers(listener); for (Map.Entry, Collection> entry : listenerMethods.asMap().entrySet()) { Class eventType = entry.getKey(); Collection eventMethodsInListener = entry.getValue(); CopyOnWriteArraySet eventSubscribers = subscribers.get(eventType); if (eventSubscribers == null) { CopyOnWriteArraySet newSet = new CopyOnWriteArraySet(); eventSubscribers = MoreObjects.firstNonNull( subscribers.putIfAbsent(eventType, newSet), newSet); } eventSubscribers.addAll(eventMethodsInListener); } } /** * Unregisters all subscribers on the given listener object. */ void unregister(Object listener) { Multimap, Subscriber> listenerMethods = findAllSubscribers(listener); for (Map.Entry, Collection> entry : listenerMethods.asMap().entrySet()) { Class eventType = entry.getKey(); Collection listenerMethodsForType = entry.getValue(); CopyOnWriteArraySet currentSubscribers = subscribers.get(eventType); if (currentSubscribers == null || !currentSubscribers.removeAll(listenerMethodsForType)) { // if removeAll returns true, all we really know is that at least one subscriber was // removed... however, barring something very strange we can assume that if at least one // subscriber was removed, all subscribers on listener for that event type were... after // all, the definition of subscribers on a particular class is totally static throw new IllegalArgumentException( "missing event subscriber for an annotated method. Is " + listener + " registered?"); } // don't try to remove the set if it's empty; that can't be done safely without a lock // anyway, if the set is empty it'll just be wrapping an array of length 0 } } @VisibleForTesting Set getSubscribersForTesting(Class eventType) { return MoreObjects.firstNonNull(subscribers.get(eventType), ImmutableSet. of()); } /** * Gets an iterator representing an immutable snapshot of all subscribers to the given event at * the time this method is called. */ Iterator getSubscribers(Object event) { ImmutableSet> eventTypes = flattenHierarchy(event.getClass()); List> subscriberIterators = Lists.newArrayListWithCapacity(eventTypes.size()); for (Class eventType : eventTypes) { CopyOnWriteArraySet eventSubscribers = subscribers.get(eventType); if (eventSubscribers != null) { // eager no-copy snapshot subscriberIterators.add(eventSubscribers.iterator()); } } return Iterators.concat(subscriberIterators.iterator()); } /** * A thread-safe cache that contains the mapping from each class to all methods in that class and * all super-classes, that are annotated with {@code @Subscribe}. The cache is shared across all * instances of this class; this greatly improves performance if multiple EventBus instances are * created and objects of the same class are registered on all of them. */ private static final LoadingCache, ImmutableList> subscriberMethodsCache = CacheBuilder.newBuilder() .weakKeys() .build(new CacheLoader, ImmutableList>() { @Override public ImmutableList load(Class concreteClass) throws Exception { return getAnnotatedMethodsNotCached(concreteClass); } }); /** * Returns all subscribers for the given listener grouped by the type of event they subscribe to. */ private Multimap, Subscriber> findAllSubscribers(Object listener) { Multimap, Subscriber> methodsInListener = HashMultimap.create(); Class clazz = listener.getClass(); for (Method method : getAnnotatedMethods(clazz)) { Class[] parameterTypes = method.getParameterTypes(); Class eventType = parameterTypes[0]; methodsInListener.put(eventType, Subscriber.create(bus, listener, method)); } return methodsInListener; } private static ImmutableList getAnnotatedMethods(Class clazz) { return subscriberMethodsCache.getUnchecked(clazz); } private static ImmutableList getAnnotatedMethodsNotCached(Class clazz) { Set> supertypes = TypeToken.of(clazz).getTypes().rawTypes(); Map identifiers = Maps.newHashMap(); for (Class supertype : supertypes) { for (Method method : supertype.getDeclaredMethods()) { if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) { // TODO(cgdecker): Should check for a generic parameter type and error out Class[] parameterTypes = method.getParameterTypes(); checkArgument(parameterTypes.length == 1, "Method %s has @Subscribe annotation but has %s parameters." + "Subscriber methods must have exactly 1 parameter.", method, parameterTypes.length); MethodIdentifier ident = new MethodIdentifier(method); if (!identifiers.containsKey(ident)) { identifiers.put(ident, method); } } } } return ImmutableList.copyOf(identifiers.values()); } /** * Global cache of classes to their flattened hierarchy of supertypes. */ private static final LoadingCache, ImmutableSet>> flattenHierarchyCache = CacheBuilder.newBuilder() .weakKeys() .build(new CacheLoader, ImmutableSet>>() { @SuppressWarnings("RedundantTypeArguments") // > is actually needed to compile @Override public ImmutableSet> load(Class concreteClass) { return ImmutableSet.> copyOf( TypeToken.of(concreteClass).getTypes().rawTypes()); } }); /** * Flattens a class's type hierarchy into a set of {@code Class} objects including all * superclasses (transitively) and all interfaces implemented by these superclasses. */ @VisibleForTesting static ImmutableSet> flattenHierarchy(Class concreteClass) { try { return flattenHierarchyCache.getUnchecked(concreteClass); } catch (UncheckedExecutionException e) { throw Throwables.propagate(e.getCause()); } } private static final class MethodIdentifier { private final String name; private final List> parameterTypes; MethodIdentifier(Method method) { this.name = method.getName(); this.parameterTypes = Arrays.asList(method.getParameterTypes()); } @Override public int hashCode() { return Objects.hash(name, parameterTypes); } @Override public boolean equals(@Nullable Object o) { if (o instanceof MethodIdentifier) { MethodIdentifier ident = (MethodIdentifier) o; return name.equals(ident.name) && parameterTypes.equals(ident.parameterTypes); } return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy