/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.events;
import io.reactivex.netty.RxNetty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Subscription;
import rx.exceptions.Exceptions;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Action2;
import rx.functions.Action3;
import rx.functions.Action4;
import rx.functions.Action5;
import rx.subscriptions.CompositeSubscription;
import rx.subscriptions.Subscriptions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
/**
* A holder for storing {@link EventListener} providing utility methods for any {@link EventSource} implementation that
* requires storing and invoking listeners.
*
* @param <T> Type of listener to store.
*/
public final class ListenersHolder<T extends EventListener> implements EventSource<T>, EventPublisher {
private static final Logger logger = LoggerFactory.getLogger(ListenersHolder.class);
private final CopyOnWriteArraySet<ListenerHolder<T>> listeners;
public ListenersHolder() {
listeners = new CopyOnWriteArraySet<>();
}
public ListenersHolder(ListenersHolder<T> toCopy) {
listeners = new CopyOnWriteArraySet<>(toCopy.listeners);
for (final ListenerHolder<T> holder : listeners) {
// Add the subscription to the existing holder, so that on unsubscribe, it is also removed from this list.
holder.subscription.add(Subscriptions.create(new Action0() {
@Override
public void call() {
listeners.remove(holder);
}
}));
}
}
@Override
public Subscription subscribe(final T listener) {
final CompositeSubscription cs = new CompositeSubscription();
ListenerHolder.configureRemoval(cs, listener, listeners);
final ListenerHolder<T> holder = new ListenerHolder<>(listener, cs);
listeners.add(holder);
return cs;
}
@Override
public boolean publishingEnabled() {
return !RxNetty.isEventPublishingDisabled() && !listeners.isEmpty();
}
public void dispose() {
ListenerInvocationException exception = null;
for (ListenerHolder<T> listener : listeners) {
try {
listener.onCompleted();
} catch (Throwable e) {
exception = handleListenerError(exception, listener, e);
}
}
if (null != exception) {
exception.finish();
throw exception;
}
}
/**
* Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary
* validations required for invoking a listener and also guards against a listener throwing exceptions on invocation.
*
* @param invocationAction The action to perform on all listeners.
*/
public void invokeListeners(Action1<T> invocationAction) {
ListenerInvocationException exception = null;
for (final ListenerHolder<T> listener : listeners) {
if (!listener.subscription.isUnsubscribed()) {
try {
invocationAction.call(listener.delegate);
} catch (Throwable e) {
exception = handleListenerError(exception, listener, e);
}
}
}
if (null != exception) {
exception.finish();
/*Do not bubble event notification errors to the caller, event notifications are best effort.*/
logger.error("Error occured while invoking event listeners.", exception);
}
}
/**
* Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary
* validations required for invoking a listener and also guards against a listener throwing exceptions on invocation.
*
* @param invocationAction The action to perform on all listeners.
* @param duration Duration.
* @param timeUnit Time unit for the duration.
*/
public void invokeListeners(Action3<T, Long, TimeUnit> invocationAction, long duration, TimeUnit timeUnit) {
ListenerInvocationException exception = null;
for (ListenerHolder<T> listener : listeners) {
if (!listener.subscription.isUnsubscribed()) {
try {
invocationAction.call(listener.delegate, duration, timeUnit);
} catch (Throwable e) {
exception = handleListenerError(exception, listener, e);
}
}
}
if (null != exception) {
exception.finish();
/*Do not bubble event notification errors to the caller, event notifications are best effort.*/
logger.error("Error occured while invoking event listeners.", exception);
}
}
/**
* Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary
* validations required for invoking a listener and also guards against a listener throwing exceptions on invocation.
*
* @param invocationAction The action to perform on all listeners.
* @param duration Duration.
* @param timeUnit Time unit for the duration.
* @param throwable An error.
*/
public void invokeListeners(Action4<T, Long, TimeUnit, Throwable> invocationAction, long duration,
TimeUnit timeUnit, Throwable throwable) {
ListenerInvocationException exception = null;
for (ListenerHolder<T> listener : listeners) {
if (!listener.subscription.isUnsubscribed()) {
try {
invocationAction.call(listener.delegate, duration, timeUnit, throwable);
} catch (Throwable e) {
exception = handleListenerError(exception, listener, e);
}
}
}
if (null != exception) {
exception.finish();
/*Do not bubble event notification errors to the caller, event notifications are best effort.*/
logger.error("Error occured while invoking event listeners.", exception);
}
}
/**
* Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary
* validations required for invoking a listener and also guards against a listener throwing exceptions on invocation.
*
* @param invocationAction The action to perform on all listeners.
* @param duration Duration.
* @param timeUnit Time unit for the duration.
* @param arg Any arbitrary argument
*/
public <A> void invokeListeners(Action4<T, Long, TimeUnit, A> invocationAction, long duration,
TimeUnit timeUnit, A arg) {
ListenerInvocationException exception = null;
for (ListenerHolder<T> listener : listeners) {
if (!listener.subscription.isUnsubscribed()) {
try {
invocationAction.call(listener.delegate, duration, timeUnit, arg);
} catch (Throwable e) {
exception = handleListenerError(exception, listener, e);
}
}
}
if (null != exception) {
exception.finish();
/*Do not bubble event notification errors to the caller, event notifications are best effort.*/
logger.error("Error occured while invoking event listeners.", exception);
}
}
/**
* Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary
* validations required for invoking a listener and also guards against a listener throwing exceptions on invocation.
*
* @param invocationAction The action to perform on all listeners.
* @param duration Duration.
* @param timeUnit Time unit for the duration.
* @param throwable An error.
* @param arg Any arbitrary argument
*/
public <A> void invokeListeners(Action5<T, Long, TimeUnit, Throwable, A> invocationAction, long duration,
TimeUnit timeUnit, Throwable throwable, A arg) {
ListenerInvocationException exception = null;
for (ListenerHolder<T> listener : listeners) {
if (!listener.subscription.isUnsubscribed()) {
try {
invocationAction.call(listener.delegate, duration, timeUnit, throwable, arg);
} catch (Throwable e) {
exception = handleListenerError(exception, listener, e);
}
}
}
if (null != exception) {
exception.finish();
/*Do not bubble event notification errors to the caller, event notifications are best effort.*/
logger.error("Error occured while invoking event listeners.", exception);
}
}
/**
* Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary
* validations required for invoking a listener and also guards against a listener throwing exceptions on invocation.
*
* @param invocationAction The action to perform on all listeners.
* @param arg Any arbitrary argument
*/
public <A> void invokeListeners(Action2<T, A> invocationAction, A arg) {
ListenerInvocationException exception = null;
for (ListenerHolder<T> listener : listeners) {
if (!listener.subscription.isUnsubscribed()) {
try {
invocationAction.call(listener.delegate, arg);
} catch (Throwable e) {
exception = handleListenerError(exception, listener, e);
}
}
}
if (null != exception) {
exception.finish();
/*Do not bubble event notification errors to the caller, event notifications are best effort.*/
logger.error("Error occured while invoking event listeners.", exception);
}
}
/**
* Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary
* validations required for invoking a listener and also guards against a listener throwing exceptions on invocation.
*
* @param invocationAction The action to perform on all listeners.
* @param throwable An error.
* @param arg Any arbitrary argument
*/
public <A> void invokeListeners(Action3<T, Throwable, A> invocationAction, Throwable throwable, A arg) {
ListenerInvocationException exception = null;
for (ListenerHolder<T> listener : listeners) {
if (!listener.subscription.isUnsubscribed()) {
try {
invocationAction.call(listener.delegate, throwable, arg);
} catch (Throwable e) {
exception = handleListenerError(exception, listener, e);
}
}
}
if (null != exception) {
exception.finish();
/*Do not bubble event notification errors to the caller, event notifications are best effort.*/
logger.error("Error occured while invoking event listeners.", exception);
}
}
private ListenerInvocationException handleListenerError(ListenerInvocationException exception,
ListenerHolder<T> listener, Throwable e) {
Exceptions.throwIfFatal(e);
if (null == exception) {
exception = new ListenerInvocationException();
}
exception.addException(listener.delegate, e);
return exception;
}
public ListenersHolder<T> copy() {
return new ListenersHolder<>(this);
}
/*Visible for testing*/Collection<T> getAllListeners() {
final Collection<T> toReturn = new ArrayList<>();
for (ListenerHolder<T> listener : listeners) {
toReturn.add(listener.delegate);
}
return toReturn;
}
/*Visible for testing*/CopyOnWriteArraySet<ListenerHolder<T>> getActualListenersList() {
return listeners;
}
public void subscribeAllTo(EventSource<T> lazySource) {
for (ListenerHolder<T> listener : listeners) {
listener.subscription.add(lazySource.subscribe(listener.delegate));
}
}
private static class ListenerHolder<T extends EventListener> implements EventListener {
private static final CompositeSubscription EMPTY_SUB_FOR_REMOVAL = new CompositeSubscription();
private final T delegate;
private final CompositeSubscription subscription;
public ListenerHolder(T delegate, CompositeSubscription subscription) {
this.delegate = delegate;
this.subscription = subscription;
}
@Override
public void onCompleted() {
if (!subscription.isUnsubscribed()) {
try {
delegate.onCompleted();
} finally {
subscription.unsubscribe();
}
}
}
@Override
public void onCustomEvent(Object event) { }
@Override
public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { }
@Override
public void onCustomEvent(Object event, Throwable throwable) { }
@Override
public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { }
public static <X extends EventListener> ListenerHolder<X> forRemoval(X listenerToRemove) {
return new ListenerHolder<>(listenerToRemove, EMPTY_SUB_FOR_REMOVAL);
}
public static <X extends EventListener> void configureRemoval(CompositeSubscription cs,
final X listenerToRemove,
final CopyOnWriteArraySet<ListenerHolder<X>> removeFrom) {
cs.add(Subscriptions.create(new Action0() {
@Override
public void call() {
/**
* Why do we add {@link ListenerHolder} but remove {@link X}?
* Since {@link ListenerHolder} requires the associated {@link Subscription}, and then
* {@link Subscription} will require the {@link ListenerHolder}, there will be a circular dependency.
*
* Instead, by having {@link ListenerHolder} implement equals/hashcode to only look for the
* enclosing {@link X} instance, it is possible to add {@link ListenerHolder} but remove {@link X}
*/
removeFrom.remove(forRemoval(listenerToRemove));
}
}));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ListenerHolder)) {
return false;
}
@SuppressWarnings("rawtypes")
ListenerHolder that = (ListenerHolder) o;
return delegate.equals(that.delegate);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
}
}