com.fitbur.guava.common.eventbus.SubscriberRegistry Maven / Gradle / Ivy
/*
* Copyright (C) 2014 The Guava Authors
*
* 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.fitbur.guava.common.eventbus;
import static com.fitbur.guava.common.base.Preconditions.checkArgument;
import static com.fitbur.guava.common.base.Preconditions.checkNotNull;
import com.fitbur.guava.common.annotations.VisibleForTesting;
import com.fitbur.guava.common.base.MoreObjects;
import com.fitbur.guava.common.base.Objects;
import com.fitbur.guava.common.base.Throwables;
import com.fitbur.guava.common.cache.CacheBuilder;
import com.fitbur.guava.common.cache.CacheLoader;
import com.fitbur.guava.common.cache.LoadingCache;
import com.fitbur.guava.common.collect.HashMultimap;
import com.fitbur.guava.common.collect.ImmutableList;
import com.fitbur.guava.common.collect.ImmutableSet;
import com.fitbur.guava.common.collect.Iterators;
import com.fitbur.guava.common.collect.Lists;
import com.fitbur.guava.common.collect.Maps;
import com.fitbur.guava.common.collect.Multimap;
import com.fitbur.guava.common.reflect.TypeToken;
import com.fitbur.guava.common.util.concurrent.UncheckedExecutionException;
import com.fitbur.guava.j2objc.annotations.Weak;
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.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nullable;
/**
* 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 extends Class>> 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.hashCode(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;
}
}
}