001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.event.support;
020
021import org.apache.shiro.event.EventBus;
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.concurrent.locks.Lock;
033import java.util.concurrent.locks.ReentrantReadWriteLock;
034
035/**
036 * A default event bus implementation that synchronously publishes events to registered listeners.  Listeners can be
037 * registered or unregistered for events as necessary.
038 * <p/>
039 * An event bus enables a publish/subscribe paradigm within Shiro - components can publish or consume events they
040 * find relevant without needing to be tightly coupled to other components.  This affords great
041 * flexibility within Shiro by promoting loose coupling and high cohesion between components and a much safer
042 * pluggable architecture that is more resilient to change over time.
043 * <h2>Sending Events</h2>
044 * If a component wishes to publish events to other components:
045 * <pre>
046 *     MyEvent myEvent = createMyEvent();
047 *     eventBus.publish(myEvent);
048 * </pre>
049 * The event bus will determine the type of event and then dispatch the event to components that wish to receive
050 * events of that type.
051 * <h2>Receiving Events</h2>
052 * A component can receive events of interest by doing the following.
053 * <ol>
054 * <li>For each type of event you wish to consume, create a public method that accepts a single event argument.
055 * The method argument type indicates the type of event to receive.</li>
056 * <li>Annotate each of these public methods with the {@link org.apache.shiro.event.Subscribe Subscribe} annotation.</li>
057 * <li>Register the component with the event bus:
058 * <pre>
059 *         eventBus.register(myComponent);
060 *     </pre>
061 * </li>
062 * </ol>
063 * After registering the component, when when an event of a respective type is published, the component's
064 * {@code Subscribe}-annotated method(s) will be invoked as expected.
065 *
066 * This design (and its constituent helper components) was largely influenced by
067 * Guava's <a href="http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/eventbus/EventBus.html">EventBus</a>
068 * concept, although no code was shared/imported (even though Guava is Apache 2.0 licensed and could have
069 * been used).
070 *
071 * This implementation is thread-safe and may be used concurrently.
072 *
073 * @since 1.3
074 */
075public class DefaultEventBus implements EventBus {
076
077    private static final Logger log = LoggerFactory.getLogger(DefaultEventBus.class);
078
079    private static final String EVENT_LISTENER_ERROR_MSG = "Event listener processing failed.  Listeners should " +
080            "generally handle exceptions directly and not propagate to the event bus.";
081
082    //this is stateless, we can retain a static final reference:
083    private static final EventListenerComparator EVENT_LISTENER_COMPARATOR = new EventListenerComparator();
084
085    private EventListenerResolver eventListenerResolver;
086
087    //We want to preserve registration order to deliver events to objects in the order that they are registered
088    //with the event bus.  This has the nice effect that any Shiro system-level components that are registered first
089    //(likely to happen upon startup) have precedence over those registered by end-user components later.
090    //
091    //One might think that this could have been done by just using a ConcurrentSkipListMap (which is available only on
092    //JDK 6 or later).  However, this approach requires the implementation of a Comparator to sort elements, and this
093    //surfaces a problem: for any given random event listener, there isn't any guaranteed property to exist that can be
094    //inspected to determine order of registration, since registration order is an artifact of this EventBus
095    //implementation, not the listeners themselves.
096    //
097    //Therefore, we use a simple concurrent lock to wrap a LinkedHashMap - the LinkedHashMap retains insertion order
098    //and the lock provides thread-safety in probably a much simpler mechanism than attempting to write a
099    //EventBus-specific Comparator.  This technique is also likely to be faster than a ConcurrentSkipListMap, which
100    //is about 3-5 times slower than a standard ConcurrentMap.
101    private final Map<Object, Subscription> registry;
102    private final Lock registryReadLock;
103    private final Lock registryWriteLock;
104
105    public DefaultEventBus() {
106        this.registry = new LinkedHashMap<Object, Subscription>(); //not thread safe, so we need locks:
107        ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
108        this.registryReadLock = rwl.readLock();
109        this.registryWriteLock = rwl.writeLock();
110        this.eventListenerResolver = new AnnotationEventListenerResolver();
111    }
112
113    public EventListenerResolver getEventListenerResolver() {
114        return eventListenerResolver;
115    }
116
117    public void setEventListenerResolver(EventListenerResolver eventListenerResolver) {
118        this.eventListenerResolver = eventListenerResolver;
119    }
120
121    public void publish(Object event) {
122        if (event == null) {
123            log.info("Received null event for publishing.  Ignoring and returning.");
124            return;
125        }
126
127        registryReadLock.lock();
128        try {
129            //performing the entire iteration within the lock will be a slow operation if the registry has a lot of
130            //contention.  However, it is expected that the very large majority of cases the registry will be
131            //read-mostly with very little writes (registrations or removals) occurring during a typical application
132            //lifetime.
133            //
134            //The alternative would be to copy the registry.values() collection to a new LinkedHashSet within the lock
135            //only and the iteration on this new collection could be outside the lock.  This has the performance penalty
136            //however of always creating a new collection every time an event is published,  which could be more
137            //costly for the majority of applications, especially if the number of listeners is large.
138            //
139            //Finally, the read lock is re-entrant, so multiple publish calls will be
140            //concurrent without penalty since publishing is a read-only operation on the registry.
141
142            for (Subscription subscription : this.registry.values()) {
143                subscription.onEvent(event);
144            }
145        } finally {
146            registryReadLock.unlock();
147        }
148    }
149
150    public void register(Object instance) {
151        if (instance == null) {
152            log.info("Received null instance for event listener registration.  Ignoring registration request.");
153            return;
154        }
155
156        unregister(instance);
157
158        List<EventListener> listeners = getEventListenerResolver().getEventListeners(instance);
159
160        if (listeners == null || listeners.isEmpty()) {
161            log.warn("Unable to resolve event listeners for subscriber instance [{}]. Ignoring registration request.",
162                    instance);
163            return;
164        }
165
166        Subscription subscription = new Subscription(listeners);
167
168        this.registryWriteLock.lock();
169        try {
170            this.registry.put(instance, subscription);
171        } finally {
172            this.registryWriteLock.unlock();
173        }
174    }
175
176    public void unregister(Object instance) {
177        if (instance == null) {
178            return;
179        }
180        this.registryWriteLock.lock();
181        try {
182            this.registry.remove(instance);
183        } finally {
184            this.registryWriteLock.unlock();
185        }
186    }
187
188    private class Subscription {
189
190        private final List<EventListener> listeners;
191
192        public Subscription(List<EventListener> listeners) {
193            List<EventListener> toSort = new ArrayList<EventListener>(listeners);
194            Collections.sort(toSort, EVENT_LISTENER_COMPARATOR);
195            this.listeners = toSort;
196        }
197
198        public void onEvent(Object event) {
199
200            Set<Object> delivered = new HashSet<Object>();
201
202            for (EventListener listener : this.listeners) {
203                Object target = listener;
204                if (listener instanceof SingleArgumentMethodEventListener) {
205                    SingleArgumentMethodEventListener singleArgListener = (SingleArgumentMethodEventListener) listener;
206                    target = singleArgListener.getTarget();
207                }
208                if (listener.accepts(event) && !delivered.contains(target)) {
209                    try {
210                        listener.onEvent(event);
211                    } catch (Throwable t) {
212                        log.warn(EVENT_LISTENER_ERROR_MSG, t);
213                    }
214                    delivered.add(target);
215                }
216            }
217        }
218    }
219}