/*
* 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 ro.nextreports.designer.ui.eventbus;
import java.util.Collection;
import java.util.EventObject;
import java.util.LinkedList;
import javax.swing.SwingUtilities;
/**
* A default implementation of an <code>EventBus</code>. This implementation
* is threadsafe so only one event bus is needed in the system.
* <code>Event</code>s published to the bus will be broadcasted to
* <code>Subscriber</code>s in the reverse order of the subscription.
*
* @author Decebal Suiu
*/
public final class DefaultEventBus implements EventBus {
/**
* Current subscriptions to the bus.
*/
protected final Collection<Subscription> subscriptions;
/**
* Cached copy of subscriptions so we can avoid copying during event firing.
*/
protected Subscription[] subscriptionsArray;
/**
* The error listener for the eventbus
*/
protected BusListener busListener;
private boolean async;
/**
* Constructs the <code>EventBus</code>.
*/
public DefaultEventBus() {
this(true);
}
public DefaultEventBus(final boolean asynchronous) {
async = asynchronous;
subscriptions = new LinkedList<Subscription>();
}
public void setErrorListener(BusListener listener) {
busListener = listener;
}
/**
* Publishes the supplied event to the bus. All subscribers that have
* indicated their interest in the class of the event and for whom the event
* passes their <code>Filter</code> will be <code>eventError</code>ed
* of the event. <p/> This implementation notifies subscribers in the
* reverse order of the subscription. The last subscriber registered will be
* the first notified of the event.
*
* @param event
* The event to broadcast
*
* @see Filter
* @see Subscriber#inform(EventObject)
*/
public void publish(final EventObject event) {
fireEventPublished(event);
final EventDispatcher dispatcher = new EventDispatcher(event);
if (async) {
SwingUtilities.invokeLater(dispatcher);
} else {
try {
SwingUtilities.invokeAndWait(dispatcher);
} catch (Exception t) {
fireEventError(event, t);
}
}
}
/**
* Publish supplied event, but block until event subscribers have processed
* the event.
*
* @see EventBus#publish(EventObject)
*/
public void publishAndWait(EventObject event) {
fireEventPublished(event);
final EventDispatcher dispatcher = new EventDispatcher(event);
if (!SwingUtilities.isEventDispatchThread()) {
try {
SwingUtilities.invokeAndWait(dispatcher);
} catch (Exception t) {
fireEventError(event, t);
}
} else {
dispatcher.run();
}
}
private void fireEventPublished(final EventObject event) {
if (null != busListener) {
busListener.eventPublished(event);
}
}
private void fireEventError(EventObject event, Exception e) {
if (busListener != null) {
busListener.eventError(event, e);
}
}
/**
* Registers a <code>Subscriber</code> with the bus for notification of
* <code>Event</code>s of the specified type and all its subtypes. <br>
* The bus will use the supplied <code>Filter</code> prior to notification
* to verify the interest of the subscriber in the event.
*
* @param eventType
* The type of event for which the object is subscribing, which
* must be a subtype of {@link java.util.EventObject}*
* @param filter
* The filter to apply to each event, or null if events should
* not be filtered
* @param subscriber
* The object subscribing to the bus
*
* @throws IllegalArgumentException
* if eventType is not a subtype of <code>EventObject</code>
*/
public void subscribe(final Class eventType, final Filter filter,
final Subscriber subscriber) {
checkSubscriberInfo(eventType, subscriber);
final Subscription subscription = new Subscription(eventType, filter, subscriber);
synchronized (this) {
subscriptions.add(subscription);
subscriptionsArray = null;
}
}
private void checkSubscriberInfo(final Class eventType,
final Subscriber subscriber) {
if (!EventObject.class.isAssignableFrom(eventType)) {
throw new IllegalArgumentException("Invalid event class: "
+ eventType.getName());
}
if (null == subscriber) {
throw new IllegalArgumentException("'subscriber' cannot be empty.");
}
}
/**
* Removes a specific subscription from the bus. <br>
* Both the event type and filter are checked as well as the subscriber, so
* it is possible to selectively unsubscribe from any of the individual
* subscriptions the subscriber object had already performed. If there is no
* event type/filter/subscriber tuple that matches the arguments to this
* function, the bus is left unaffected.
*
* @param eventType
* The type of event to match
* @param filter
* The filter to match
* @param subscriber
* The subscriber to match
*
* @throws IllegalArgumentException
* if eventType is not a subtype of <code>EventObject</code>
*/
public void unsubscribe(final Class eventType, final Filter filter,
final Subscriber subscriber) {
checkSubscriberInfo(eventType, subscriber);
final Subscription subscription = new Subscription(eventType, filter, subscriber);
synchronized (this) {
subscriptions.remove(subscription);
subscriptionsArray = null;
}
}
/**
* Encapsulate the event and dispatch it when we can.
*
* @author Decebal Suiu
*/
class EventDispatcher implements Runnable {
private final EventObject event;
/**
* Create an EventDispatcher.
*
* @param event
* The event we are going to dispatch
*/
public EventDispatcher(final EventObject event) {
this.event = event;
}
/**
* Iterate through the subscriptions and send the event to the proper
* destinations.
*/
public void run() {
// Ensure we are not steping on each others toes
synchronized (DefaultEventBus.this) {
if (subscriptionsArray == null) {
subscriptionsArray = (Subscription[]) subscriptions
.toArray(new Subscription[0]);
}
// Iterate subscriptions in reverse order
for (int i = 0; i < subscriptionsArray.length; i++) {
final Subscription subscription = subscriptionsArray[i];
if ((subscription.getEventType().isAssignableFrom(event.getClass()))
&& (subscription.getFilter() == null || subscription
.getFilter().apply(event))) {
try {
subscription.getSubscriber().inform(event);
} catch (Exception e) {
fireEventError(event, e);
}
}
}
}
}
}
/**
* Represents a subscription to the bus.
*
* @author Decebal Suiu
*/
static final class Subscription {
private final Class eventType;
private final Filter filter;
private final Subscriber subscriber;
/**
* Create the Subscription object.
*
* @param eventType
* The type of event to listen for
* @param filter
* The filter used to check
* @param subscriber
* The subscriber that receives notification
*/
public Subscription(final Class eventType, final Filter filter,
final Subscriber subscriber) {
this.eventType = eventType;
this.filter = filter;
this.subscriber = subscriber;
}
/**
* Get the EventType.
*
* @return The event type
*/
public Class getEventType() {
return eventType;
}
/**
* Get the filter used with this subscription.
*
* @return The filter
*/
public Filter getFilter() {
return filter;
}
/**
* Get the subscriber.
*
* @return the subscriber
*/
public Subscriber getSubscriber() {
return subscriber;
}
/**
* Compare two Subscriptions to each other.
*
* @param o
* The other object
*
* @return <code>true</code> if the two Subscription objects are the
* same
*/
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Subscription)) {
return false;
}
final Subscription subscription = (Subscription) o;
if (!eventType.equals(subscription.eventType)) {
return false;
}
if (filter != null ? !filter.equals(subscription.filter)
: subscription.filter != null) {
return false;
}
if (!subscriber.equals(subscription.subscriber)) {
return false;
}
return true;
}
/**
* Get the hashcode (used in HashMaps). <p/> hashCode = 37 * (37 * (629 +
* event.hashCode()) + filter.hashCode()) + subscriber.hashCode()
*
* @return the hashCode value
*/
public int hashCode() {
int result = 17;
result = 37 * result + eventType.hashCode();
result = 37 * result + (filter != null ? filter.hashCode() : 0);
result = 37 * result + subscriber.hashCode();
return result;
}
}
}