All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ocadotechnology.notification.NotificationBus Maven / Gradle / Ivy

There is a newer version: 16.6.21
Show newest version
/*
 * Copyright © 2017-2023 Ocado (Ocava)
 *
 * 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 com.ocadotechnology.notification;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.annotation.concurrent.GuardedBy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.eventbus.BlockingEventBus;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.reflect.TypeToken;
import com.ocadotechnology.validation.Failer;

public abstract class NotificationBus {
    public static final String NOTIFICATION_BUS_ID = "NOTIFICATION_BUS";

    private final Logger logger = LoggerFactory.getLogger(NOTIFICATION_BUS_ID);

    private final Class notificationClass;

    private final AtomicReference thread = new AtomicReference<>(null);

    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    @GuardedBy("rwLock")
    private BlockingEventBus eventBus;

    /** All the explicit registrations via {@link #addHandler(Object)}. */
    @GuardedBy("rwLock")
    private final Map, Class> registeredNotifications = new HashMap<>();

    /** Cache: Only touched by {@link #isParentOfNotificationRegistered}
* Can be cleared at any time using {@link #clearCache()} with no behavioural change. */ @GuardedBy("rwLock") private final Map, Boolean> cacheOfImpliedNotifications = new HashMap<>(); private final PointToPointValidator pointToPointValidator = new PointToPointValidator(); protected NotificationBus(Class notificationClass) { this.notificationClass = notificationClass; eventBus = new BlockingEventBus(); } /** This method is and needs to remain ThreadSafe. */ protected void addHandler(Object handler) { List> newNotifications = collectSubscribingTypes(handler); pointToPointValidator.validate(handler, newNotifications); Lock lock = rwLock.writeLock(); try { lock.lock(); newNotifications.forEach(type -> registeredNotifications.put(type, type)); clearCache(); // Not sure that eventBus is thread-safe, so we'll include it in our lock eventBus.register(handler); } finally { lock.unlock(); } } private List> collectSubscribingTypes(Object handler) { List> newNotifications = new ArrayList<>(8); Class clazz = handler.getClass(); Set> supertypes = TypeToken.of(clazz).getTypes().rawTypes(); for (Class supertype : supertypes) { for (Method method : supertype.getDeclaredMethods()) { if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) { Class[] parameterTypes = method.getParameterTypes(); Preconditions.checkArgument(parameterTypes.length == 1, "@Subscribe-annotated handler method %s should have a single parameter", method.getName()); Class notificationType = parameterTypes[0]; Preconditions.checkArgument(notificationClass.isAssignableFrom(notificationType), "Can not register notification %s from %s handler %s. Only %s notifications are allowed", notificationType.getSimpleName(), clazz.getSimpleName(), method.getName(), notificationClass.getSimpleName()); newNotifications.add(notificationType); } } } return newNotifications; } public void clearAllHandlers() { pointToPointValidator.reset(); Lock lock = rwLock.writeLock(); try { lock.lock(); registeredNotifications.clear(); clearCache(); clearThread(); eventBus = new BlockingEventBus(); } finally { lock.unlock(); } } public void broadcast(N notification) { EventBus bus; Lock lock = rwLock.readLock(); try { lock.lock(); // This has to go in a readLock (even though we use a volatile because of clearAllHandler) checkThatThisBusHasOnlyBeenUsedByOneThread(notification); bus = eventBus; } finally { lock.unlock(); } if (logger.isTraceEnabled()) { logger.trace("{} broadcasting {}", getClass().getSimpleName(), notification); } bus.post(notification); } public void clearThread() { Lock lock = rwLock.writeLock(); try { lock.lock(); thread.set(null); } finally { lock.unlock(); } } protected void replaceAllNotifications(Collection> newNotifications) { Lock lock = rwLock.writeLock(); try { lock.lock(); registeredNotifications.clear(); newNotifications.forEach(n -> registeredNotifications.put(n, n)); clearCache(); } finally { lock.unlock(); } } protected void checkThatThisBusHasOnlyBeenUsedByOneThread(N notification) { Thread current = Thread.currentThread(); if (current == thread.get()) { return; // expected path (always true, except first time and errors). Having this short-cut saves approx 5ns per call } Thread permitted = thread.updateAndGet(t -> (t == null) ? current : t); if (current == permitted) { return; } throw Failer.fail("first Thread: %s [%s] current Thread: %s [%s] %s", permitted, permitted.getId(), current, current.getId(), notification); } protected boolean isNotificationRegistered(Class notification) { Lock lock = rwLock.readLock(); try { lock.lock(); if (registeredNotifications.containsKey(notification)) { return true; } Boolean result = cacheOfImpliedNotifications.get(notification); if (result != null) { return result; } } finally { lock.unlock(); } // Has to be done outside readLock as can't upgrade read to write return isParentOfNotificationRegistered(notification); } /** Have we already asked this question?
* If not, check if we've registered any parents (and cache the answer). */ private boolean isParentOfNotificationRegistered(Class notification) { Set> classes = TypeToken.of(notification).getTypes().rawTypes(); Lock lock = rwLock.writeLock(); try { lock.lock(); for (Class type : classes) { if (registeredNotifications.containsKey(type)) { // check must be in lock // registeredNotification contains only notifications we want, so this is always true: cacheOfImpliedNotifications.put(notification, Boolean.TRUE); return true; } } // We haven't registered this notification or any of it's parents, so cache the failure: cacheOfImpliedNotifications.put(notification, Boolean.FALSE); return false; } finally { lock.unlock(); } } private void clearCache() { cacheOfImpliedNotifications.clear(); } protected boolean canHandleNotification(Class notification) { return hasCorrectType(notification); } protected abstract boolean hasCorrectType(Class notification); @Override public String toString() { return MoreObjects.toStringHelper(this) .add("notificationClass", notificationClass) .add("registeredNotifications", registeredNotifications) .toString(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy