You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shiro.apache.org by bd...@apache.org on 2016/07/11 21:04:30 UTC

[03/13] shiro git commit: SHIRO-395: added initial implementation - all org.apache.shiro.event.** additions are at 100% test coverage.

SHIRO-395: added initial implementation - all org.apache.shiro.event.** additions are at 100% test coverage.

git-svn-id: https://svn.apache.org/repos/asf/shiro/trunk@1410707 13f79535-47bb-0310-9956-ffa450edef68


Project: http://git-wip-us.apache.org/repos/asf/shiro/repo
Commit: http://git-wip-us.apache.org/repos/asf/shiro/commit/bbc8efac
Tree: http://git-wip-us.apache.org/repos/asf/shiro/tree/bbc8efac
Diff: http://git-wip-us.apache.org/repos/asf/shiro/diff/bbc8efac

Branch: refs/heads/1.3.x
Commit: bbc8efaccef60939c8786e317966c878a34203ce
Parents: 0a886db
Author: Les Hazlewood <lh...@apache.org>
Authored: Sat Nov 17 08:12:48 2012 +0000
Committer: Brian Demers <bd...@apache.org>
Committed: Fri Jul 8 13:49:37 2016 -0400

----------------------------------------------------------------------
 .../java/org/apache/shiro/event/Publisher.java  |  34 ++
 .../java/org/apache/shiro/event/Subscribe.java  |  45 ++
 .../bus/AnnotationEventListenerResolver.java    |  96 +++++
 .../apache/shiro/event/bus/ClassComparator.java |  73 ++++
 .../apache/shiro/event/bus/DefaultEventBus.java | 161 ++++++++
 .../apache/shiro/event/bus/EventListener.java   |  56 +++
 .../event/bus/EventListenerComparator.java      |  68 ++++
 .../shiro/event/bus/EventListenerResolver.java  |  50 +++
 .../bus/SingleArgumentMethodEventListener.java  |  74 ++++
 .../shiro/event/bus/SubscriberRegistry.java     |  45 ++
 .../shiro/event/bus/TypedEventListener.java     |  27 ++
 .../main/java/org/apache/shiro/util/Assert.java | 407 +++++++++++++++++++
 .../java/org/apache/shiro/util/ClassUtils.java  |  27 ++
 .../AnnotationEventListenerResolverTest.groovy  |  45 ++
 .../org/apache/shiro/event/bus/BarEvent.groovy  |  29 ++
 .../org/apache/shiro/event/bus/BazEvent.groovy  |  29 ++
 .../shiro/event/bus/ClassComparatorTest.groovy  |  62 +++
 .../shiro/event/bus/DefaultEventBusTest.groovy  | 163 ++++++++
 .../bus/ErroneouslyAnnotatedSubscriber.groovy   |  31 ++
 .../shiro/event/bus/ErrorCausingEvent.groovy    |  25 ++
 .../bus/EventListenerComparatorTest.groovy      |  71 ++++
 .../bus/ExceptionThrowingSubscriber.groovy      |  32 ++
 .../org/apache/shiro/event/bus/FooEvent.groovy  |  29 ++
 .../event/bus/NotAnnotatedSubscriber.groovy     |  27 ++
 .../apache/shiro/event/bus/SimpleEvent.groovy   |  29 ++
 .../shiro/event/bus/SimpleSubscriber.groovy     |  38 ++
 ...SingleArgumentMethodEventListenerTest.groovy |  86 ++++
 .../event/bus/SubclassTestSubscriber.groovy     |  49 +++
 .../shiro/event/bus/TestSubscriber.groovy       |  50 +++
 29 files changed, 1958 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/Publisher.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/Publisher.java b/core/src/main/java/org/apache/shiro/event/Publisher.java
new file mode 100644
index 0000000..50c1c8d
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/Publisher.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event;
+
+/**
+ * Publishes events to an event subsystem that will deliver events to registered {@link Subscribe}rs.
+ *
+ * @since 1.3
+ */
+public interface Publisher {
+
+    /**
+     * Publishes the specified event to an event subsystem that will deliver events to relevant {@link Subscribe}rs.
+     *
+     * @param event The event object to distribute to relevant subscribers.
+     */
+    void publish(Object event);
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/Subscribe.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/Subscribe.java b/core/src/main/java/org/apache/shiro/event/Subscribe.java
new file mode 100644
index 0000000..03bc599
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/Subscribe.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates a method is an event consumer.  The method must have a single argument and the argument's type determines
+ * what type of events should be delivered to the method for consumption.
+ * <p/>
+ * For example:
+ * <pre>
+ * &#64;Subscribe
+ * public void onSomeEvent(SomeEvent event) { ... }
+ * </pre>
+ * <p/>
+ * Because the method argument is declared as a {@code SomeEvent} type, the method will be called by the event
+ * dispatcher whenever a {@code SomeEvent} instance (or one of its subclass instances that is not already registered)
+ * is published.
+ *
+ * @since 1.3
+ */
+@Retention(value = RetentionPolicy.RUNTIME)
+@Target(value = ElementType.METHOD)
+public @interface Subscribe {
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/AnnotationEventListenerResolver.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/AnnotationEventListenerResolver.java b/core/src/main/java/org/apache/shiro/event/bus/AnnotationEventListenerResolver.java
new file mode 100644
index 0000000..883dbcf
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/AnnotationEventListenerResolver.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+import org.apache.shiro.event.Subscribe;
+import org.apache.shiro.util.ClassUtils;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Inspects an object for annotated methods of interest and creates an {@link EventListener} for each method discovered.
+ * An event bus will call the resulting listeners as relevant events arrive.
+ * <p/>
+ * The default {@link #setAnnotationClass(Class) annotationClass} is {@link Subscribe}, indicating any
+ * {@link Subscribe}-annotated method will be represented as an EventListener.
+ *
+ * @see SingleArgumentMethodEventListener
+ * @since 1.3
+ */
+public class AnnotationEventListenerResolver implements EventListenerResolver {
+
+    private Class<? extends Annotation> annotationClass;
+
+    public AnnotationEventListenerResolver() {
+        this.annotationClass = Subscribe.class;
+    }
+
+    /**
+     * Returns a new collection of {@link EventListener} instances, each instance corresponding to an annotated
+     * method discovered on the specified {@code instance} argument.
+     *
+     * @param instance the instance to inspect for annotated event handler methods.
+     * @return a new collection of {@link EventListener} instances, each instance corresponding to an annotated
+     *         method discovered on the specified {@code instance} argument.
+     */
+    public List<EventListener> getEventListeners(Object instance) {
+        if (instance == null) {
+            return Collections.emptyList();
+        }
+
+        List<Method> methods = ClassUtils.getAnnotatedMethods(instance.getClass(), getAnnotationClass());
+        if (methods == null || methods.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<EventListener> listeners = new ArrayList<EventListener>(methods.size());
+
+        for (Method m : methods) {
+            listeners.add(new SingleArgumentMethodEventListener(instance, m));
+        }
+
+        return listeners;
+    }
+
+    /**
+     * Returns the type of annotation that indicates a method that should be represented as an {@link EventListener},
+     * defaults to {@link Subscribe}.
+     *
+     * @return the type of annotation that indicates a method that should be represented as an {@link EventListener},
+     *         defaults to {@link Subscribe}.
+     */
+    public Class<? extends Annotation> getAnnotationClass() {
+        return annotationClass;
+    }
+
+    /**
+     * Sets the type of annotation that indicates a method that should be represented as an {@link EventListener}.
+     * The default value is {@link Subscribe}.
+     *
+     * @param annotationClass the type of annotation that indicates a method that should be represented as an
+     *                        {@link EventListener}.  The default value is {@link Subscribe}.
+     */
+    public void setAnnotationClass(Class<? extends Annotation> annotationClass) {
+        this.annotationClass = annotationClass;
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/ClassComparator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/ClassComparator.java b/core/src/main/java/org/apache/shiro/event/bus/ClassComparator.java
new file mode 100644
index 0000000..4d1928f
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/ClassComparator.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+import java.util.Comparator;
+
+/**
+ * Compares two classes based on their position in a hierarchy.  Classes higher up in a hierarchy are 'greater than'
+ * (ordered later) than classes lower in a hierarchy (ordered earlier).  Classes in unrelated hierarchies have the same
+ * order priority.
+ * <p/>
+ * Event bus implementations use this comparator to determine which event listener method to invoke when polymorphic
+ * listener methods are defined:
+ * <p/>
+ * If two event classes exist A and B, where A is the parent class of B (and B is a subclass of A) and an event
+ * subscriber listens to both events:
+ * <pre>
+ * &#64;Subscribe
+ * public void onEvent(A a) { ... }
+ *
+ * &#64;Subscribe
+ * public void onEvent(B b) { ... }
+ * </pre>
+ *
+ * The {@code onEvent(B b)} method will be invoked on the subscriber and the
+ * {@code onEvent(A a)} method will <em>not</em> be invoked.  This is to prevent multiple dispatching of a single event
+ * to the same consumer.
+ * <p/>
+ * The ClassComparator is used to order listener method priority based on their event argument class - methods handling
+ * event subclasses have higher precedence than superclasses.
+ *
+ * @since 1.3
+ */
+public class ClassComparator implements Comparator<Class> {
+
+    public int compare(Class a, Class b) {
+        if (a == null) {
+            if (b == null) {
+                return 0;
+            } else {
+                return -1;
+            }
+        } else if (b == null) {
+            return 1;
+        } else if (a == b || a.equals(b)) {
+            return 0;
+        } else {
+            if (a.isAssignableFrom(b)) {
+                return 1;
+            } else if (b.isAssignableFrom(a)) {
+                return -1;
+            } else {
+                return 0;
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/DefaultEventBus.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/DefaultEventBus.java b/core/src/main/java/org/apache/shiro/event/bus/DefaultEventBus.java
new file mode 100644
index 0000000..4b74522
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/DefaultEventBus.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+
+import org.apache.shiro.event.Publisher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A default event bus implementation that synchronously publishes events to registered listeners.  Listeners can be
+ * registered or unregistered for events as necessary.
+ * <p/>
+ * An event bus enables a publish/subscribe paradigm within Shiro - components can publish or consume events they
+ * find relevant without needing to be tightly coupled to other components.  This affords great
+ * flexibility within Shiro by promoting loose coupling and high cohesion between components and a much safer pluggable
+ * architecture.
+ * <h2>Sending Events</h2>
+ * If a component wishes to publish events to other components:
+ * <pre>
+ *     MyEvent myEvent = createMyEvent();
+ *     eventBus.publish(myEvent);
+ * </pre>
+ * The event bus will determine the type of event and then dispatch the event to components that wish to receive
+ * events of that type.
+ * <h2>Receiving Events</h2>
+ * A component can receive events of interest by doing the following.
+ * <ol>
+ *     <li>For each type of event you wish to consume, create a public method that accepts a single event argument.
+ *     The method argument type indicates the type of event to receive.</li>
+ *     <li>Annotate each of these public methods with the {@link org.apache.shiro.event.Subscribe Subscribe} annotation.</li>
+ *     <li>Register the component with the event bus:
+ *     <pre>
+ *         eventBus.register(myComponent);
+ *     </pre>
+ *     </li>
+ * </ol>
+ * After registering the component, when when an event of a respective type is published, the component's
+ * {@code Subscribe}-annotated method(s) will be invoked as expected.
+ * <p/>
+ * This design (and its constituent helper components) was largely influenced by
+ * Guava's <a href="http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/eventbus/EventBus.html">EventBus</a>
+ * concept, although no code was shared/imported (even though Guava is Apache 2.0 licensed and could have
+ * been used).
+ *
+ * @since 1.3
+ */
+public class DefaultEventBus implements Publisher, SubscriberRegistry {
+
+    private static final Logger log = LoggerFactory.getLogger(DefaultEventBus.class);
+
+    private EventListenerResolver eventListenerResolver;
+
+    private final Map<Object,Subscriber> registry;
+
+    public DefaultEventBus() {
+        this.registry = new ConcurrentHashMap<Object, Subscriber>();
+        this.eventListenerResolver = new AnnotationEventListenerResolver();
+    }
+
+    public EventListenerResolver getEventListenerResolver() {
+        return eventListenerResolver;
+    }
+
+    public void setEventListenerResolver(EventListenerResolver eventListenerResolver) {
+        this.eventListenerResolver = eventListenerResolver;
+    }
+
+    public void publish(Object event) {
+        if (event == null) {
+            log.info("Received null event for publishing.  Ignoring and returning.");
+            return;
+        }
+
+        for( Subscriber subscriber : registry.values() ) {
+            subscriber.onEvent(event);
+        }
+    }
+
+    public void register(Object instance) {
+        if (instance == null) {
+            log.info("Received null instance for registration.  Ignoring registration request.");
+            return;
+        }
+
+        unregister(instance);
+
+        List<EventListener> listeners = getEventListenerResolver().getEventListeners(instance);
+
+        if (listeners == null || listeners.isEmpty()) {
+            log.warn("Unable to resolve any event listeners for the subscriber instance [" + instance +
+                    "].  Ignoring registration request.");
+            return;
+        }
+
+        Subscriber subscriber = new Subscriber(instance, listeners);
+
+        this.registry.put(instance, subscriber);
+    }
+
+    public void unregister(Object instance) {
+        if (instance == null) {
+            return;
+        }
+        this.registry.remove(instance);
+    }
+
+    private class Subscriber {
+
+        private final Object instance;
+        private final List<EventListener> registeredListeners;
+
+        public Subscriber(Object instance, List<EventListener> listeners) {
+            this.instance = instance;
+            List<EventListener> toSort = new ArrayList<EventListener>(listeners);
+            Collections.sort(toSort, new EventListenerComparator());
+            this.registeredListeners = toSort;
+        }
+
+        public void onEvent(Object event) {
+
+            Set<Object> delivered = new HashSet<Object>();
+
+            for(EventListener listener : this.registeredListeners) {
+                Object target = listener;
+                if (listener instanceof SingleArgumentMethodEventListener) {
+                    SingleArgumentMethodEventListener singleArgListener = (SingleArgumentMethodEventListener)listener;
+                    target = singleArgListener.getTarget();
+                }
+                if (listener.accepts(event) && !delivered.contains(target)) {
+                    try {
+                        listener.onEvent(event);
+                        delivered.add(target);
+                    } catch (Throwable t) {
+                        log.warn("Event listener processing failed.  Listeners should generally " +
+                                "handle exceptions directly and not propagate to the event bus.", t);
+                    }
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/EventListener.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/EventListener.java b/core/src/main/java/org/apache/shiro/event/bus/EventListener.java
new file mode 100644
index 0000000..a653674
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/EventListener.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+/**
+ * An event listener knows how to accept and process events of a particular type (or types).
+ * <p/>
+ * Note that this interface is in the event bus package (and not the event package directly) because it is a supporting
+ * concept for event bus implementations and not something that application
+ * developers using Shiro should implement directly.  App developers should instead use the
+ * {@link org.apache.shiro.event.Subscribe Subscribe} annotation on methods they wish to receive events.
+ * <p/>
+ * This interface therefore mainly represents a 'middle man' between the event bus and the actual subscribing
+ * component.  As such, event bus implementors (or framework/infrastructural implementors) or those that wish to
+ * customize listener/dispatch functionality might find this concept useful.
+ * <p/>
+ * It is a concept almost always used in conjunction with a {@link EventListenerResolver} implementation.
+ *
+ * @see SingleArgumentMethodEventListener
+ * @see AnnotationEventListenerResolver
+ *
+ * @since 1.3
+ */
+public interface EventListener {
+
+    /**
+     * Returns {@code true} if the listener instance can process the specified event object, {@code false} otherwise.
+     * @param event the event object to test
+     * @return {@code true} if the listener instance can process the specified event object, {@code false} otherwise.
+     */
+    boolean accepts(Object event);
+
+    /**
+     * Handles the specified event.  Again, as this interface is an implementation concept, implementations of this
+     * method will likely dispatch the event to a 'real' processor (e.g. method).
+     *
+     * @param event the event to handle.
+     */
+    void onEvent(Object event);
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/EventListenerComparator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/EventListenerComparator.java b/core/src/main/java/org/apache/shiro/event/bus/EventListenerComparator.java
new file mode 100644
index 0000000..d096591
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/EventListenerComparator.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+import java.util.Comparator;
+
+/**
+ * Compares two event listeners to determine the order in which they should be invoked when an event is dispatched.
+ * The lower the order, the sooner it will be invoked (the higher its precedence).  The higher the order, the later
+ * it will be invoked (the lower its precedence).
+ * <p/>
+ * TypedEventListeners have a higher precedence (i.e. a lower order) than standard EventListener instances.  Standard
+ * EventListener instances have the same order priority.
+ * <p/>
+ * When both objects being compared are TypedEventListeners, they are ordered according to the rules of the
+ * {@link ClassComparator}, using the TypedEventListeners'
+ * {@link org.apache.shiro.event.bus.TypedEventListener#getEventType() eventType}.
+ *
+ * @since 1.3
+ */
+public class EventListenerComparator implements Comparator<EventListener> {
+
+    public int compare(EventListener a, EventListener b) {
+        if (a == null) {
+            if (b == null) {
+                return 0;
+            } else {
+                return -1;
+            }
+        } else if (b == null) {
+            return 1;
+        } else if (a == b || a.equals(b)) {
+            return 0;
+        } else {
+            if (a instanceof TypedEventListener) {
+                TypedEventListener ta = (TypedEventListener)a;
+                if (b instanceof TypedEventListener) {
+                    TypedEventListener tb = (TypedEventListener)b;
+                    return new ClassComparator().compare(ta.getEventType(), tb.getEventType());
+                } else {
+                    return -1; //TypedEventListeners are 'less than' (higher priority) than non typed
+                }
+            } else {
+                if (b instanceof TypedEventListener) {
+                    return 1;
+                } else {
+                    return 0;
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/EventListenerResolver.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/EventListenerResolver.java b/core/src/main/java/org/apache/shiro/event/bus/EventListenerResolver.java
new file mode 100644
index 0000000..b865d89
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/EventListenerResolver.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+import java.util.List;
+
+/**
+ * An {@code EventListenerResolver} knows how to resolve (either create or lookup) {@link EventListener} instances
+ * as a result of inspecting a subscriber object, mostly likely a
+ * {@link org.apache.shiro.event.Subscribe Subscribe}-annotated object instance.
+ * <p/>
+ * This interface exists primarily as a support concept for the {@link DefaultEventBus} implementation.  Custom
+ * implementations of this interface can be configured on a {@link DefaultEventBus} instance to determine exactly
+ * how a subscriber receives events.
+ * <p/>
+ * For example, the {@link AnnotationEventListenerResolver AnnotationEventListenerResolver} will inspect a runtime
+ * object for {@link org.apache.shiro.event.Subscribe Subscribe}-annotated methods, and for each method found, return
+ * an {@link EventListener} instance representing the method to invoke.
+ *
+ * @see AnnotationEventListenerResolver
+ * @see SingleArgumentMethodEventListener
+ * @since 1.3
+ */
+public interface EventListenerResolver {
+
+    /**
+     * Returns {@link EventListener} instances as a result of inspecting a subscriber object, mostly likely with
+     * {@link org.apache.shiro.event.Subscribe Subscribe}-annotated methods.
+     *
+     * @param instance the subscriber instance for which EventListener instances should be acquired.
+     * @return {@link EventListener} instances as a result of inspecting a subscriber object.
+     */
+    List<EventListener> getEventListeners(Object instance);
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/SingleArgumentMethodEventListener.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/SingleArgumentMethodEventListener.java b/core/src/main/java/org/apache/shiro/event/bus/SingleArgumentMethodEventListener.java
new file mode 100644
index 0000000..f5ea9f9
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/SingleArgumentMethodEventListener.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+import java.lang.reflect.Method;
+
+/**
+ * A event listener that invokes a target object's method that accepts a single event argument.
+ *
+ * @since 1.3
+ */
+public class SingleArgumentMethodEventListener implements TypedEventListener {
+
+    private final Object target;
+    private final Method method;
+
+    public SingleArgumentMethodEventListener(Object target, Method method) {
+        this.target = target;
+        this.method = method;
+        //assert that the method is defined as expected:
+        getMethodArgumentType(method);
+    }
+
+    public Object getTarget() {
+        return this.target;
+    }
+
+    public Method getMethod() {
+        return this.method;
+    }
+
+    public boolean accepts(Object event) {
+        return event != null && getEventType().isInstance(event);
+    }
+
+    public Class getEventType() {
+        return getMethodArgumentType(getMethod());
+    }
+
+    public void onEvent(Object event) {
+        Method method = getMethod();
+        try {
+            method.invoke(getTarget(), event);
+        } catch (Exception e) {
+            throw new IllegalStateException("Unable to invoke event handler method [" + method + "]", e);
+        }
+    }
+
+    protected Class getMethodArgumentType(Method method) {
+        Class[] paramTypes = method.getParameterTypes();
+        if (paramTypes.length != 1) {
+            //the default implementation expects a single typed argument and nothing more:
+            String msg = "Event handler methods must accept a single argument.";
+            throw new IllegalArgumentException(msg);
+        }
+        return paramTypes[0];
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/SubscriberRegistry.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/SubscriberRegistry.java b/core/src/main/java/org/apache/shiro/event/bus/SubscriberRegistry.java
new file mode 100644
index 0000000..af57d6e
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/SubscriberRegistry.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+/**
+ * Allows event subscribers to register or unregister with an event subsystem to receive (or not receive) published
+ * events.
+ *
+ * @since 1.3
+ */
+public interface SubscriberRegistry {
+
+    /**
+     * Registers all event handler methods on the specified instance to receive relevant events.  The handler methods
+     * are determined by {@link SubscriberRegistry} implementations, typically by using an
+     * {@link EventListenerResolver} (e.g. {@link AnnotationEventListenerResolver}).
+     *
+     * @param subscriber the object whose event handler methods should be registered to receive events.
+     */
+    void register(Object subscriber);
+
+    /**
+     * Unregisters all previously-registered event handler methods on the specified instance.  If the specified object
+     * was not previously registered, calling this method has no effect.
+     *
+     * @param subscriber the previously
+     */
+    void unregister(Object subscriber);
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/event/bus/TypedEventListener.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/event/bus/TypedEventListener.java b/core/src/main/java/org/apache/shiro/event/bus/TypedEventListener.java
new file mode 100644
index 0000000..9cb51e0
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/event/bus/TypedEventListener.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus;
+
+/**
+ * @since 1.3
+ */
+public interface TypedEventListener extends EventListener {
+
+    Class getEventType();
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/util/Assert.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/util/Assert.java b/core/src/main/java/org/apache/shiro/util/Assert.java
new file mode 100644
index 0000000..d580247
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/util/Assert.java
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.util;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Assertion utility class that assists in validating arguments.
+ * Useful for identifying programmer errors early and clearly at runtime.
+ * Usage also reduces a program's
+ * <a href="http://en.wikipedia.org/wiki/Cyclomatic_complexity">cyclomatic complexity</a>.
+ *
+ * <p>For example, if the contract of a public method states it does not
+ * allow <code>null</code> arguments, Assert can be used to validate that
+ * contract. Doing this clearly indicates a contract violation when it
+ * occurs and protects the class's invariants.
+ *
+ * <p>Typically used to validate method arguments rather than configuration
+ * properties, to check for cases that are usually programmer errors rather than
+ * configuration errors. In contrast to config initialization code, there is
+ * usally no point in falling back to defaults in such methods.
+ *
+ * <p>This class is similar to JUnit's assertion library. If an argument value is
+ * deemed invalid, an {@link IllegalArgumentException} is thrown (typically).
+ * For example:
+ *
+ * <pre class="code">
+ * Assert.notNull(clazz, "The class must not be null");
+ * Assert.isTrue(i > 0, "The value must be greater than zero");</pre>
+ *
+ * Mainly for internal use within the framework; consider Jakarta's Commons Lang
+ * >= 2.0 for a more comprehensive suite of assertion utilities.
+ * <p/>
+ * <em>Gratefully borrowed from the Spring Framework, also Apache 2.0 licensed</em>
+ *
+ * @author Keith Donald
+ * @author Juergen Hoeller
+ * @author Colin Sampaleanu
+ * @author Rob Harrop
+ * @since 1.3
+ */
+public abstract class Assert {
+
+    /**
+     * Assert a boolean expression, throwing <code>IllegalArgumentException</code>
+     * if the test result is <code>false</code>.
+     * <pre class="code">Assert.isTrue(i &gt; 0, "The value must be greater than zero");</pre>
+     * @param expression a boolean expression
+     * @param message the exception message to use if the assertion fails
+     * @throws IllegalArgumentException if expression is <code>false</code>
+     */
+    public static void isTrue(boolean expression, String message) {
+        if (!expression) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert a boolean expression, throwing <code>IllegalArgumentException</code>
+     * if the test result is <code>false</code>.
+     * <pre class="code">Assert.isTrue(i &gt; 0);</pre>
+     * @param expression a boolean expression
+     * @throws IllegalArgumentException if expression is <code>false</code>
+     */
+    public static void isTrue(boolean expression) {
+        isTrue(expression, "[Assertion failed] - this expression must be true");
+    }
+
+    /**
+     * Assert that an object is <code>null</code> .
+     * <pre class="code">Assert.isNull(value, "The value must be null");</pre>
+     * @param object the object to check
+     * @param message the exception message to use if the assertion fails
+     * @throws IllegalArgumentException if the object is not <code>null</code>
+     */
+    public static void isNull(Object object, String message) {
+        if (object != null) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert that an object is <code>null</code> .
+     * <pre class="code">Assert.isNull(value);</pre>
+     * @param object the object to check
+     * @throws IllegalArgumentException if the object is not <code>null</code>
+     */
+    public static void isNull(Object object) {
+        isNull(object, "[Assertion failed] - the object argument must be null");
+    }
+
+    /**
+     * Assert that an object is not <code>null</code> .
+     * <pre class="code">Assert.notNull(clazz, "The class must not be null");</pre>
+     * @param object the object to check
+     * @param message the exception message to use if the assertion fails
+     * @throws IllegalArgumentException if the object is <code>null</code>
+     */
+    public static void notNull(Object object, String message) {
+        if (object == null) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert that an object is not <code>null</code> .
+     * <pre class="code">Assert.notNull(clazz);</pre>
+     * @param object the object to check
+     * @throws IllegalArgumentException if the object is <code>null</code>
+     */
+    public static void notNull(Object object) {
+        notNull(object, "[Assertion failed] - this argument is required; it must not be null");
+    }
+
+    /**
+     * Assert that the given String is not empty; that is,
+     * it must not be <code>null</code> and not the empty String.
+     * <pre class="code">Assert.hasLength(name, "Name must not be empty");</pre>
+     * @param text the String to check
+     * @param message the exception message to use if the assertion fails
+     * @see StringUtils#hasLength
+     */
+    public static void hasLength(String text, String message) {
+        if (!StringUtils.hasLength(text)) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert that the given String is not empty; that is,
+     * it must not be <code>null</code> and not the empty String.
+     * <pre class="code">Assert.hasLength(name);</pre>
+     * @param text the String to check
+     * @see StringUtils#hasLength
+     */
+    public static void hasLength(String text) {
+        hasLength(text,
+                "[Assertion failed] - this String argument must have length; it must not be null or empty");
+    }
+
+    /**
+     * Assert that the given String has valid text content; that is, it must not
+     * be <code>null</code> and must contain at least one non-whitespace character.
+     * <pre class="code">Assert.hasText(name, "'name' must not be empty");</pre>
+     * @param text the String to check
+     * @param message the exception message to use if the assertion fails
+     * @see StringUtils#hasText
+     */
+    public static void hasText(String text, String message) {
+        if (!StringUtils.hasText(text)) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert that the given String has valid text content; that is, it must not
+     * be <code>null</code> and must contain at least one non-whitespace character.
+     * <pre class="code">Assert.hasText(name, "'name' must not be empty");</pre>
+     * @param text the String to check
+     * @see StringUtils#hasText
+     */
+    public static void hasText(String text) {
+        hasText(text,
+                "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank");
+    }
+
+    /**
+     * Assert that the given text does not contain the given substring.
+     * <pre class="code">Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");</pre>
+     * @param textToSearch the text to search
+     * @param substring the substring to find within the text
+     * @param message the exception message to use if the assertion fails
+     */
+    public static void doesNotContain(String textToSearch, String substring, String message) {
+        if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) &&
+                textToSearch.indexOf(substring) != -1) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert that the given text does not contain the given substring.
+     * <pre class="code">Assert.doesNotContain(name, "rod");</pre>
+     * @param textToSearch the text to search
+     * @param substring the substring to find within the text
+     */
+    public static void doesNotContain(String textToSearch, String substring) {
+        doesNotContain(textToSearch, substring,
+                "[Assertion failed] - this String argument must not contain the substring [" + substring + "]");
+    }
+
+
+    /**
+     * Assert that an array has elements; that is, it must not be
+     * <code>null</code> and must have at least one element.
+     * <pre class="code">Assert.notEmpty(array, "The array must have elements");</pre>
+     * @param array the array to check
+     * @param message the exception message to use if the assertion fails
+     * @throws IllegalArgumentException if the object array is <code>null</code> or has no elements
+     */
+    public static void notEmpty(Object[] array, String message) {
+        if (array == null || array.length == 0) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert that an array has elements; that is, it must not be
+     * <code>null</code> and must have at least one element.
+     * <pre class="code">Assert.notEmpty(array);</pre>
+     * @param array the array to check
+     * @throws IllegalArgumentException if the object array is <code>null</code> or has no elements
+     */
+    public static void notEmpty(Object[] array) {
+        notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element");
+    }
+
+    /**
+     * Assert that an array has no null elements.
+     * Note: Does not complain if the array is empty!
+     * <pre class="code">Assert.noNullElements(array, "The array must have non-null elements");</pre>
+     * @param array the array to check
+     * @param message the exception message to use if the assertion fails
+     * @throws IllegalArgumentException if the object array contains a <code>null</code> element
+     */
+    public static void noNullElements(Object[] array, String message) {
+        if (array != null) {
+            for (int i = 0; i < array.length; i++) {
+                if (array[i] == null) {
+                    throw new IllegalArgumentException(message);
+                }
+            }
+        }
+    }
+
+    /**
+     * Assert that an array has no null elements.
+     * Note: Does not complain if the array is empty!
+     * <pre class="code">Assert.noNullElements(array);</pre>
+     * @param array the array to check
+     * @throws IllegalArgumentException if the object array contains a <code>null</code> element
+     */
+    public static void noNullElements(Object[] array) {
+        noNullElements(array, "[Assertion failed] - this array must not contain any null elements");
+    }
+
+    /**
+     * Assert that a collection has elements; that is, it must not be
+     * <code>null</code> and must have at least one element.
+     * <pre class="code">Assert.notEmpty(collection, "Collection must have elements");</pre>
+     * @param collection the collection to check
+     * @param message the exception message to use if the assertion fails
+     * @throws IllegalArgumentException if the collection is <code>null</code> or has no elements
+     */
+    public static void notEmpty(Collection collection, String message) {
+        if (CollectionUtils.isEmpty(collection)) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert that a collection has elements; that is, it must not be
+     * <code>null</code> and must have at least one element.
+     * <pre class="code">Assert.notEmpty(collection, "Collection must have elements");</pre>
+     * @param collection the collection to check
+     * @throws IllegalArgumentException if the collection is <code>null</code> or has no elements
+     */
+    public static void notEmpty(Collection collection) {
+        notEmpty(collection,
+                "[Assertion failed] - this collection must not be empty: it must contain at least 1 element");
+    }
+
+    /**
+     * Assert that a Map has entries; that is, it must not be <code>null</code>
+     * and must have at least one entry.
+     * <pre class="code">Assert.notEmpty(map, "Map must have entries");</pre>
+     * @param map the map to check
+     * @param message the exception message to use if the assertion fails
+     * @throws IllegalArgumentException if the map is <code>null</code> or has no entries
+     */
+    public static void notEmpty(Map map, String message) {
+        if (CollectionUtils.isEmpty(map)) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Assert that a Map has entries; that is, it must not be <code>null</code>
+     * and must have at least one entry.
+     * <pre class="code">Assert.notEmpty(map);</pre>
+     * @param map the map to check
+     * @throws IllegalArgumentException if the map is <code>null</code> or has no entries
+     */
+    public static void notEmpty(Map map) {
+        notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry");
+    }
+
+
+    /**
+     * Assert that the provided object is an instance of the provided class.
+     * <pre class="code">Assert.instanceOf(Foo.class, foo);</pre>
+     * @param clazz the required class
+     * @param obj the object to check
+     * @throws IllegalArgumentException if the object is not an instance of clazz
+     * @see Class#isInstance
+     */
+    public static void isInstanceOf(Class clazz, Object obj) {
+        isInstanceOf(clazz, obj, "");
+    }
+
+    /**
+     * Assert that the provided object is an instance of the provided class.
+     * <pre class="code">Assert.instanceOf(Foo.class, foo);</pre>
+     * @param type the type to check against
+     * @param obj the object to check
+     * @param message a message which will be prepended to the message produced by
+     * the function itself, and which may be used to provide context. It should
+     * normally end in a ": " or ". " so that the function generate message looks
+     * ok when prepended to it.
+     * @throws IllegalArgumentException if the object is not an instance of clazz
+     * @see Class#isInstance
+     */
+    public static void isInstanceOf(Class type, Object obj, String message) {
+        notNull(type, "Type to check against must not be null");
+        if (!type.isInstance(obj)) {
+            throw new IllegalArgumentException(message +
+                    "Object of class [" + (obj != null ? obj.getClass().getName() : "null") +
+                    "] must be an instance of " + type);
+        }
+    }
+
+    /**
+     * Assert that <code>superType.isAssignableFrom(subType)</code> is <code>true</code>.
+     * <pre class="code">Assert.isAssignable(Number.class, myClass);</pre>
+     * @param superType the super type to check
+     * @param subType the sub type to check
+     * @throws IllegalArgumentException if the classes are not assignable
+     */
+    public static void isAssignable(Class superType, Class subType) {
+        isAssignable(superType, subType, "");
+    }
+
+    /**
+     * Assert that <code>superType.isAssignableFrom(subType)</code> is <code>true</code>.
+     * <pre class="code">Assert.isAssignable(Number.class, myClass);</pre>
+     * @param superType the super type to check against
+     * @param subType the sub type to check
+     * @param message a message which will be prepended to the message produced by
+     * the function itself, and which may be used to provide context. It should
+     * normally end in a ": " or ". " so that the function generate message looks
+     * ok when prepended to it.
+     * @throws IllegalArgumentException if the classes are not assignable
+     */
+    public static void isAssignable(Class superType, Class subType, String message) {
+        notNull(superType, "Type to check against must not be null");
+        if (subType == null || !superType.isAssignableFrom(subType)) {
+            throw new IllegalArgumentException(message + subType + " is not assignable to " + superType);
+        }
+    }
+
+
+    /**
+     * Assert a boolean expression, throwing <code>IllegalStateException</code>
+     * if the test result is <code>false</code>. Call isTrue if you wish to
+     * throw IllegalArgumentException on an assertion failure.
+     * <pre class="code">Assert.state(id == null, "The id property must not already be initialized");</pre>
+     * @param expression a boolean expression
+     * @param message the exception message to use if the assertion fails
+     * @throws IllegalStateException if expression is <code>false</code>
+     */
+    public static void state(boolean expression, String message) {
+        if (!expression) {
+            throw new IllegalStateException(message);
+        }
+    }
+
+    /**
+     * Assert a boolean expression, throwing {@link IllegalStateException}
+     * if the test result is <code>false</code>.
+     * <p>Call {@link #isTrue(boolean)} if you wish to
+     * throw {@link IllegalArgumentException} on an assertion failure.
+     * <pre class="code">Assert.state(id == null);</pre>
+     * @param expression a boolean expression
+     * @throws IllegalStateException if the supplied expression is <code>false</code>
+     */
+    public static void state(boolean expression) {
+        state(expression, "[Assertion failed] - this state invariant must be true");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/main/java/org/apache/shiro/util/ClassUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/shiro/util/ClassUtils.java b/core/src/main/java/org/apache/shiro/util/ClassUtils.java
index dd46b06..aa7334b 100644
--- a/core/src/main/java/org/apache/shiro/util/ClassUtils.java
+++ b/core/src/main/java/org/apache/shiro/util/ClassUtils.java
@@ -22,7 +22,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.InputStream;
+import java.lang.annotation.Annotation;
 import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
 
 
 /**
@@ -208,6 +212,29 @@ public class ClassUtils {
     }
 
     /**
+     *
+     * @param type
+     * @param annotation
+     * @return
+     * @since 1.3
+     */
+    public static List<Method> getAnnotatedMethods(final Class<?> type, final Class<? extends Annotation> annotation) {
+        final List<Method> methods = new ArrayList<Method>();
+        Class<?> clazz = type;
+        while (!Object.class.equals(clazz)) {
+            Method[] currentClassMethods = clazz.getDeclaredMethods();
+            for (final Method method : currentClassMethods) {
+                if (annotation == null || method.isAnnotationPresent(annotation)) {
+                    methods.add(method);
+                }
+            }
+            // move to the upper class in the hierarchy in search for more methods
+            clazz = clazz.getSuperclass();
+        }
+        return methods;
+    }
+
+    /**
      * @since 1.0
      */
     private static interface ClassLoaderAccessor {

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/AnnotationEventListenerResolverTest.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/AnnotationEventListenerResolverTest.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/AnnotationEventListenerResolverTest.groovy
new file mode 100644
index 0000000..de32bca
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/AnnotationEventListenerResolverTest.groovy
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+/**
+ * @since 1.3
+ */
+class AnnotationEventListenerResolverTest extends GroovyTestCase {
+
+    void testSetGetAnnotationClass() {
+        def resolver = new AnnotationEventListenerResolver();
+        resolver.setAnnotationClass(Override.class) //any old annotation will do for the test
+        assertSame Override.class, resolver.getAnnotationClass()
+    }
+
+    void testNullInstanceArgument() {
+        def resolver = new AnnotationEventListenerResolver()
+        def collection = resolver.getEventListeners(null)
+        assertNotNull collection
+        assertTrue collection.isEmpty()
+    }
+
+    void testNoAnnotationsArgument() {
+        def resolver = new AnnotationEventListenerResolver()
+        def collection = resolver.getEventListeners(new NotAnnotatedSubscriber())
+        assertNotNull collection
+        assertTrue collection.isEmpty()
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/BarEvent.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/BarEvent.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/BarEvent.groovy
new file mode 100644
index 0000000..7123bec
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/BarEvent.groovy
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+/**
+ * @since 1.3
+ */
+class BarEvent extends FooEvent {
+
+    BarEvent(Object o) {
+        super(o)
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/BazEvent.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/BazEvent.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/BazEvent.groovy
new file mode 100644
index 0000000..60a90ca
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/BazEvent.groovy
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+/**
+ * @since 1.3
+ */
+class BazEvent extends BarEvent {
+
+    BazEvent(Object o) {
+        super(o)
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/ClassComparatorTest.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/ClassComparatorTest.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/ClassComparatorTest.groovy
new file mode 100644
index 0000000..6302d98
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/ClassComparatorTest.groovy
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+/**
+ * @since 1.3
+ */
+class ClassComparatorTest extends GroovyTestCase {
+
+    ClassComparator comparator
+
+    @Override
+    protected void setUp() {
+        comparator = new ClassComparator()
+    }
+
+    void testANull() {
+        def result = comparator.compare(null, Object)
+        assertEquals(-1, result)
+    }
+
+    void testBNull() {
+        def result = comparator.compare(Object, null)
+        assertEquals 1, result
+    }
+
+    void testBothNull() {
+        assertEquals 0, comparator.compare(null, null)
+    }
+
+    void testBothSame() {
+        assertEquals 0, comparator.compare(Object, Object)
+    }
+
+    void testAParentOfB() {
+        assertEquals 1, comparator.compare(Number, Integer)
+    }
+
+    void testBParentOfA() {
+        assertEquals(-1, comparator.compare(Integer, Number))
+    }
+
+    void testUnrelated() {
+        assertEquals(0, comparator.compare(Integer, Boolean))
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/DefaultEventBusTest.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/DefaultEventBusTest.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/DefaultEventBusTest.groovy
new file mode 100644
index 0000000..c29b69d
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/DefaultEventBusTest.groovy
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+import static org.easymock.EasyMock.*
+
+/**
+ * @since 1.3
+ */
+class DefaultEventBusTest extends GroovyTestCase {
+
+    DefaultEventBus bus;
+
+    @Override
+    protected void setUp() {
+        bus = new DefaultEventBus()
+    }
+
+    void testSetEventListenerResolver() {
+        def resolver = new EventListenerResolver() {
+            List<EventListener> getEventListeners(Object instance) {
+                return null //dummy implementation
+            }
+        }
+        bus.setEventListenerResolver(resolver)
+        assertSame resolver, bus.getEventListenerResolver()
+    }
+
+    void testSimpleSubscribe() {
+        def subscriber = new TestSubscriber();
+
+        bus.register(subscriber);
+
+        def event = new FooEvent(this)
+
+        bus.publish(event)
+
+        assertEquals 1, subscriber.fooCount
+        assertEquals 1, subscriber.count
+        assertSame event, subscriber.lastEvent
+    }
+
+    void testPublishNullEvent() {
+        def subscriber = new TestSubscriber();
+        bus.register(subscriber)
+
+        bus.publish(null)
+
+        assertEquals 0, subscriber.count
+    }
+
+    void testSubscribeNullInstance() {
+        def resolver = createStrictMock(EventListenerResolver)  //assert no methods are called on this
+        bus.eventListenerResolver = resolver
+
+        replay(resolver)
+
+        bus.register(null)
+
+        verify(resolver)
+    }
+
+    void testSubscribeWithoutAnnotations() {
+        def subscriber = new NotAnnotatedSubscriber()
+        bus.register(subscriber)
+
+        bus.publish(new FooEvent(this))
+
+        assertEquals 0, bus.registry.size()
+    }
+
+    void testUnsubscribeNullInstance() {
+        bus.unregister(null)
+    }
+
+    void testUnsubscribe() {
+        def subscriber = new TestSubscriber()
+        bus.register(subscriber)
+        assertEquals 1, bus.registry.size()
+
+        def event = new FooEvent(this)
+
+        bus.publish(event)
+
+        assertSame event, subscriber.lastEvent
+        assertEquals 1, subscriber.fooCount
+        assertEquals 1, subscriber.count
+
+        bus.unregister(subscriber)
+
+        assertEquals 0, bus.registry.size()
+    }
+
+    void testPolymorphicSubscribeMethodsOnlyOneInvoked() {
+        def subscriber = new TestSubscriber()
+        bus.register(subscriber)
+
+        def event = new BarEvent(this)
+
+        bus.publish(event)
+
+        assertSame event, subscriber.lastEvent
+        assertEquals 0, subscriber.fooCount
+        assertEquals 1, subscriber.barCount
+        assertEquals 1, subscriber.count
+    }
+
+    void testPolymorphicSubscribeMethodsOnlyOneInvokedWithListenerSubclass() {
+        def subscriber = new SubclassTestSubscriber()
+        bus.register(subscriber)
+
+        def event = new BazEvent(this)
+
+        bus.publish(event)
+
+        assertSame event, subscriber.lastEvent
+        assertEquals 1, subscriber.count
+        assertEquals 1, subscriber.bazCount
+        assertEquals 0, subscriber.fooCount
+        assertEquals 0, subscriber.barCount
+    }
+
+    void testSubscribeWithErroneousAnnotation() {
+        def subscriber = new ErroneouslyAnnotatedSubscriber()
+        //noinspection GroovyUnusedCatchParameter
+        try {
+            bus.register(subscriber)
+            fail("exception expected")
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    void testContinueThroughListenerExceptions() {
+        def ok = new SimpleSubscriber()
+        def error = new ExceptionThrowingSubscriber()
+
+        bus.register(ok)
+        bus.register(error)
+
+        bus.publish(new ErrorCausingEvent())
+        bus.publish(new SimpleEvent())
+
+        assertEquals 1, ok.count
+        assertEquals 0, error.count
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/ErroneouslyAnnotatedSubscriber.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/ErroneouslyAnnotatedSubscriber.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/ErroneouslyAnnotatedSubscriber.groovy
new file mode 100644
index 0000000..82546a6
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/ErroneouslyAnnotatedSubscriber.groovy
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+import org.apache.shiro.event.Subscribe
+
+/**
+ * @since 1.3
+ */
+class ErroneouslyAnnotatedSubscriber {
+
+    @Subscribe
+    void onEvent(FooEvent event, Object someOtherArg) {
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/ErrorCausingEvent.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/ErrorCausingEvent.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/ErrorCausingEvent.groovy
new file mode 100644
index 0000000..897399f
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/ErrorCausingEvent.groovy
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+/**
+ * @since 1.3
+ */
+class ErrorCausingEvent {
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/EventListenerComparatorTest.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/EventListenerComparatorTest.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/EventListenerComparatorTest.groovy
new file mode 100644
index 0000000..1615e46
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/EventListenerComparatorTest.groovy
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+import static org.easymock.EasyMock.createStrictMock
+
+/**
+ * @since 1.3
+ */
+class EventListenerComparatorTest extends GroovyTestCase {
+
+    EventListenerComparator comparator
+
+    @Override
+    protected void setUp() {
+        comparator = new EventListenerComparator()
+    }
+
+    void testANull() {
+        def result = comparator.compare(null, createStrictMock(EventListener))
+        assertEquals(-1, result)
+    }
+
+    void testBNull() {
+        def result = comparator.compare(createStrictMock(EventListener), null)
+        assertEquals 1, result
+    }
+
+    void testBothNull() {
+        assertEquals 0, comparator.compare(null, null)
+    }
+
+    void testBothSame() {
+        def mock = createStrictMock(EventListener)
+        assertEquals 0, comparator.compare(mock, mock)
+    }
+
+    void testBothEventListener() {
+        def a = createStrictMock(EventListener)
+        def b = createStrictMock(EventListener)
+        assertEquals 0, comparator.compare(a, b)
+    }
+
+    void testATypedListenerBNormalListener() {
+        def a = createStrictMock(TypedEventListener)
+        def b = createStrictMock(EventListener)
+        assertEquals(-1, comparator.compare(a, b))
+    }
+
+    void testANormalBTypedListener() {
+        def a = createStrictMock(EventListener)
+        def b = createStrictMock(TypedEventListener)
+        assertEquals 1, comparator.compare(a, b)
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/ExceptionThrowingSubscriber.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/ExceptionThrowingSubscriber.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/ExceptionThrowingSubscriber.groovy
new file mode 100644
index 0000000..b0b63c1
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/ExceptionThrowingSubscriber.groovy
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+import org.apache.shiro.event.Subscribe
+
+/**
+ * @since 1.3
+ */
+class ExceptionThrowingSubscriber extends TestSubscriber {
+
+    @Subscribe
+    void onEvent(ErrorCausingEvent event) {
+        throw new UnsupportedOperationException("This throws!")
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/FooEvent.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/FooEvent.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/FooEvent.groovy
new file mode 100644
index 0000000..b594c92
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/FooEvent.groovy
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+/**
+ * @since 1.3
+ */
+class FooEvent extends EventObject {
+
+    FooEvent(Object o) {
+        super(o)
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/NotAnnotatedSubscriber.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/NotAnnotatedSubscriber.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/NotAnnotatedSubscriber.groovy
new file mode 100644
index 0000000..8489185
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/NotAnnotatedSubscriber.groovy
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+/**
+ * @since 1.3
+ */
+class NotAnnotatedSubscriber {
+    //not a subscriber - no methods have been annotated on purpose.
+    void hello() {}
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/SimpleEvent.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/SimpleEvent.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/SimpleEvent.groovy
new file mode 100644
index 0000000..5639f5d
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/SimpleEvent.groovy
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: lhazlewood
+ * Date: 11/16/12
+ * Time: 11:09 PM
+ * To change this template use File | Settings | File Templates.
+ */
+class SimpleEvent {
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/SimpleSubscriber.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/SimpleSubscriber.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/SimpleSubscriber.groovy
new file mode 100644
index 0000000..0fa44ea
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/SimpleSubscriber.groovy
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+import org.apache.shiro.event.Subscribe
+
+/**
+ * @since 1.3
+ */
+class SimpleSubscriber {
+
+    int count
+
+    SimpleSubscriber() {
+        count = 0
+    }
+
+    @Subscribe
+    void onEvent(SimpleEvent event) {
+        count++
+    }
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/SingleArgumentMethodEventListenerTest.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/SingleArgumentMethodEventListenerTest.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/SingleArgumentMethodEventListenerTest.groovy
new file mode 100644
index 0000000..bf3c060
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/SingleArgumentMethodEventListenerTest.groovy
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+import java.lang.reflect.Method
+
+/**
+ * @since 1.3
+ */
+class SingleArgumentMethodEventListenerTest extends GroovyTestCase {
+
+    void testInvalidConstruction() {
+
+        def target = new Object()
+
+        def method = Object.class.getMethods()[0] //any old method will do
+
+        try {
+            //noinspection GroovyResultOfObjectAllocationIgnored
+            new SingleArgumentMethodEventListener(target, method)
+            fail("exception expected")
+        } catch (IllegalArgumentException iae) {
+            assertEquals iae.message, "Event handler methods must accept a single argument."
+        }
+    }
+
+    void testValidConstruction() {
+
+        def target = new TestSubscriber()
+        def method = TestSubscriber.class.getMethods().find { it.name == "onFooEvent" }
+
+        def listener = new SingleArgumentMethodEventListener(target, method)
+
+        assertSame target, listener.getTarget()
+        assertSame method, listener.getMethod()
+    }
+
+    void testMethodException() {
+
+        def target = new TestSubscriber()
+        def method = TestSubscriber.class.getMethods().find { it.name == "onFooEvent" }
+
+        def listener = new SingleArgumentMethodEventListener(target, method) {
+            @Override
+            Method getMethod() {
+                //sneakily swap out the valid method with an erroneous one.  This wouldn't ever happen normally, we're
+                //just doing this as a test harness:
+                return Object.class.getMethods()[0] //any method will do
+            }
+        }
+
+        //now invoke the erroneous method and ensure we get an exception:
+        try {
+            listener.onEvent(new FooEvent(this))
+            fail("exception expected")
+        } catch (IllegalStateException ise) {
+            assertTrue ise.message.startsWith("Unable to invoke event handler method")
+        }
+    }
+
+    void testAccepts() {
+        def target = new TestSubscriber()
+        def method = TestSubscriber.class.getMethods().find { it.name == "onFooEvent" }
+
+        def listener = new SingleArgumentMethodEventListener(target, method)
+
+        assertTrue listener.accepts(new FooEvent(this))
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/shiro/blob/bbc8efac/core/src/test/groovy/org/apache/shiro/event/bus/SubclassTestSubscriber.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/groovy/org/apache/shiro/event/bus/SubclassTestSubscriber.groovy b/core/src/test/groovy/org/apache/shiro/event/bus/SubclassTestSubscriber.groovy
new file mode 100644
index 0000000..8dc717c
--- /dev/null
+++ b/core/src/test/groovy/org/apache/shiro/event/bus/SubclassTestSubscriber.groovy
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shiro.event.bus
+
+import org.apache.shiro.event.Subscribe
+
+/**
+ * @since 1.3
+ */
+class SubclassTestSubscriber extends TestSubscriber {
+
+    int bazCount
+
+    SubclassTestSubscriber() {
+        bazCount = 0
+    }
+
+    @Subscribe
+    void onEvent(BazEvent event) {
+        bazCount++
+        lastEvent = event;
+    }
+
+    @Subscribe
+    void onEvent(ErrorCausingEvent event) {
+        throw new UnsupportedOperationException("This throws!")
+    }
+
+    @Override
+    int getCount() {
+        return super.getCount() + bazCount
+    }
+}