/**
* Copyright © 2014 Instituto Superior Técnico
*
* This file is part of Bennu Signals.
*
* Bennu Signals is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Bennu Signals is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Bennu Signals. If not, see <http://www.gnu.org/licenses/>.
*/
package org.fenixedu.bennu.core.signals;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import javax.transaction.Status;
import javax.transaction.SystemException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pt.ist.fenixframework.CommitListener;
import pt.ist.fenixframework.FenixFramework;
import pt.ist.fenixframework.Transaction;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
/**
* This is the main class for the signaling system. This implements a system wide event bus that allows writing code in one module
* that observes and operates over another set of code.
*
* @author Artur Ventura
*
*/
public class Signal {
private static final Logger logger = LoggerFactory.getLogger(Signal.class);
private static final Map<String, SignalEventBus> withTransaction = new ConcurrentHashMap<>();
private static final Map<String, SignalEventBus> withoutTransaction = new ConcurrentHashMap<>();
private static final CommitListener listener = new CommitListener() {
@Override
public void afterCommit(Transaction transaction) {
try {
if (transaction.getStatus() == Status.STATUS_COMMITTED) {
Signal.fireAllInCacheOutsideTransaction(transaction);
}
} catch (SystemException e) {
logger.error("Can't fire signals", e);
}
}
@Override
public void beforeCommit(Transaction transaction) {
Signal.fireAllInCacheWithinTransaction(transaction);
}
};
/**
* Initializes the signaling system. This registers the required FenixFramework commit listeners.
*/
public static void init() {
FenixFramework.getTransactionManager().addCommitListener(listener);
}
/**
* Shuts down the signaling system. This removes all registered event handlers,
* and removes the FenixFramework transactional listener.
*/
public static void shutdown() {
innerClear();
FenixFramework.getTransactionManager().removeCommitListener(listener);
}
/**
* Registers a handler for events of that key that runs within the same transaction as the emited events. This means that this
* handler can still abort the transaction.
*
* @param key
* The key in which to register this handler.
* @param handler
* The handler to register.
* @return
* The {@link HandlerRegistration} associated with this handler
*/
public static HandlerRegistration register(String key, Object handler) {
return registerInBus(key, handler, withTransaction, true);
}
/**
* Registers the given {@link Consumer} as a handler for events, with the same semantics as {@link #register(String, Object)}.
*
* The advantage of this method is that it is lambda-ready, removing the necessity of creating an {@link EventBus} compatible
* class.
*
* @param <T>
* The type of the object sent with the signal
* @param key
* The key in which to register this handler.
* @param handler
* The handler to register.
* @return
* The {@link HandlerRegistration} associated with this handler.
* @throws NullPointerException
* If either key or handler are null.
*/
public static <T> HandlerRegistration register(String key, Consumer<T> handler) {
return register(key, new LambdaHandler<T>(handler));
}
/**
* Registers a handler for events of that key. This handler will run outside the transaction and only after the commit is
* successful. If you want the chance to abort the transaction, use {@link #register(String, Object)} .
*
* @param key
* The key in which to register this handler.
* @param handler
* The handler to register.
* @return
* The {@link HandlerRegistration} associated with this handler.
*/
public static HandlerRegistration registerWithoutTransaction(String key, Object handler) {
return registerInBus(key, handler, withoutTransaction, false);
}
/**
* Registers the given {@link Consumer} as a handler for events, with the same semantics as
* {@link #registerWithoutTransaction(String, Object)}.
*
* The advantage of this method is that it is lambda-ready, removing the necessity of creating an {@link EventBus} compatible
* class.
*
* @param <T>
* The type of the object sent with the signal
* @param key
* The key in which to register this handler.
* @param handler
* The handler to register.
* @return
* The {@link HandlerRegistration} associated with this handler.
* @throws NullPointerException
* If either key or handler are null.
*/
public static <T> HandlerRegistration registerWithoutTransaction(String key, Consumer<T> handler) {
return registerWithoutTransaction(key, new LambdaHandler<T>(handler));
}
private static HandlerRegistration registerInBus(String key, Object handler, Map<String, SignalEventBus> eventBuses,
boolean throwsException) {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(handler, "handler");
eventBuses.computeIfAbsent(key, (k) -> new SignalEventBus(k, throwsException)).register(handler);
return new HandlerRegistration(key, handler);
}
/**
* Clears all handlers for a given key
*
* @param key
* the key to be cleared
*/
public static void clear(String key) {
withTransaction.remove(key);
withoutTransaction.remove(key);
}
/**
* Clears all event handlers. This method emits a warning because the uses cases for this method are mostly restricted to
* development enviroments.
*
*/
public static void clear() {
logger.warn("Detaching all handlers. Be sure what you are doing.");
innerClear();
}
private static void innerClear() {
withTransaction.clear();
withoutTransaction.clear();
}
/**
* Unregisters a handler for a given key.
*
* @param key
* the context key
* @param handler
* the handler to be removed
*/
public static void unregister(String key, Object handler) {
SignalEventBus with = withTransaction.get(key);
SignalEventBus without = withoutTransaction.get(key);
if (with != null) {
with.unregister(handler);
}
if (without != null) {
without.unregister(handler);
}
}
/**
* Unregisters a handler in all keys.
*
* @param handler to be removed
*/
public static void unregister(Object handler) {
withTransaction.forEach((key, bus) -> bus.unregister(handler));
withoutTransaction.forEach((key, bus) -> bus.unregister(handler));
}
/**
* Emits a event.
*
* @param key the key for that signal
* @param event the event object
*/
public static void emit(String key, Object event) {
if (withoutTransaction.containsKey(key)) {
Map<String, List<Object>> cache = FenixFramework.getTransaction().getFromContext("signals");
if (cache == null) {
cache = new HashMap<>();
FenixFramework.getTransaction().putInContext("signals", cache);
}
cache.computeIfAbsent(key, (k) -> new ArrayList<Object>()).add(event);
}
if (withTransaction.containsKey(key)) {
Map<String, List<Object>> cache = FenixFramework.getTransaction().getFromContext("signalsWithTransaction");
if (cache == null) {
cache = new HashMap<>();
FenixFramework.getTransaction().putInContext("signalsWithTransaction", cache);
}
cache.computeIfAbsent(key, (k) -> new ArrayList<Object>()).add(event);
}
}
private static void fireAllInCacheOutsideTransaction(Transaction transaction) {
Map<String, ArrayList<Object>> cache = transaction.getFromContext("signals");
/* allowing signal emiting within a signal */
while (cache != null) {
transaction.putInContext("signals", null);
for (Entry<String, ArrayList<Object>> entry : cache.entrySet()) {
for (Object event : entry.getValue()) {
withoutTransaction.get(entry.getKey()).emit(event);
}
}
cache = transaction.getFromContext("signals");
}
}
private static void fireAllInCacheWithinTransaction(Transaction transaction) {
Map<String, ArrayList<Object>> cache = transaction.getFromContext("signalsWithTransaction");
while (cache != null) {
transaction.putInContext("signalsWithTransaction", null);
for (Entry<String, ArrayList<Object>> entry : cache.entrySet()) {
for (Object event : entry.getValue()) {
withTransaction.get(entry.getKey()).emit(event);
}
}
cache = transaction.getFromContext("signalsWithTransaction");
}
}
private static final class LambdaHandler<T> {
private final Consumer<T> consumer;
public LambdaHandler(Consumer<T> consumer) {
this.consumer = Objects.requireNonNull(consumer);
}
@Subscribe
public void handleEvent(T event) {
this.consumer.accept(event);
}
}
}