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

com.github.robozonky.app.events.SessionEvents Maven / Gradle / Ivy

/*
 * Copyright 2020 The RoboZonky Project
 *
 * 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.github.robozonky.app.events;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import java.util.stream.Stream;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.github.robozonky.api.SessionInfo;
import com.github.robozonky.api.notifications.Event;
import com.github.robozonky.api.notifications.EventListener;
import com.github.robozonky.api.notifications.EventListenerSupplier;
import com.github.robozonky.api.notifications.SessionEvent;
import com.github.robozonky.app.events.impl.EventFactory;
import com.github.robozonky.internal.extensions.ListenerServiceLoader;
import com.github.robozonky.internal.tenant.LazyEvent;
import com.github.robozonky.internal.util.ClassUtil;

public final class SessionEvents {

    private static final Logger LOGGER = LogManager.getLogger(SessionEvents.class);
    private static final AtomicLong EVENT_COUNTER = new AtomicLong(0);
    private static final Map BY_TENANT = new ConcurrentHashMap<>(0);
    private final Map> suppliers = new ConcurrentHashMap<>(0);
    private final Set debugListeners = new CopyOnWriteArraySet<>();
    private final SessionInfo sessionInfo;
    private EventListener injectedDebugListener;

    private SessionEvents(final SessionInfo sessionInfo) {
        this.sessionInfo = sessionInfo;
        addListener(new LoggingEventFiringListener(sessionInfo));
    }

    static Collection all() { // defensive copy
        return Collections.unmodifiableCollection(new ArrayList<>(BY_TENANT.values()));
    }

    static SessionEvents forSession(final SessionInfo sessionInfo) {
        return BY_TENANT.computeIfAbsent(sessionInfo.getUsername(), i -> new SessionEvents(sessionInfo));
    }

    @SuppressWarnings("unchecked")
    static  Class getImplementingEvent(final Class original) {
        final Stream> provided = ClassUtil.getAllInterfaces(original);
        final Stream> interfaces = original.isInterface() ? // interface could be extending it directly
                Stream.concat(Stream.of(original), provided) : provided;
        final String apiPackage = "com.github.robozonky.api.notifications";
        return (Class) interfaces.filter(i -> Objects.equals(i.getPackage()
            .getName(), apiPackage))
            .filter(i -> i.getSimpleName()
                .endsWith("Event"))
            .filter(i -> !Objects.equals(i.getSimpleName(), "Event"))
            .findFirst()
            .orElseThrow();
    }

    /**
     * Takes a set of {@link Runnable}s and queues them to be fired on a background thread, in the guaranteed order of
     * appearance.
     *
     * @param futures Each item in the stream represents a singular event to be fired.
     * @return When complete, all listeners have been notified of all the events.
     */
    @SuppressWarnings("rawtypes")
    private static CompletableFuture runAsync(final Stream futures) {
        final CompletableFuture[] results = futures.map(CompletableFuture::runAsync)
            .toArray(CompletableFuture[]::new);
        return GlobalEvents.merge(results);
    }

    public SessionInfo getSessionInfo() {
        return sessionInfo;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private  List> retrieveListenerSuppliers(final Class eventType) {
        final Class impl = getImplementingEvent(eventType);
        LOGGER.trace("Event {} implements {}.", eventType, impl);
        return ListenerServiceLoader.load(sessionInfo, impl);
    }

    /**
     * Represents the payload that will be handed over to {@link #runAsync(Stream)}.
     * 
     * @param lazyEvent The event which will be instantiated and sent to the listener.
     * @param listener  The listener to receive the event.
     * @param        Type of the event.
     */
    @SuppressWarnings("unchecked")
    private  void fireAny(final LazyEvent lazyEvent, final EventListener listener) {
        final Class> listenerType = (Class>) listener.getClass();
        try {
            final T event = lazyEvent.get(); // possibly incurring performance penalties
            debugListeners.forEach(l -> l.ready(event, listenerType));
            listener.handle(event, sessionInfo);
            debugListeners.forEach(l -> l.fired(event, listenerType));
        } catch (final Exception ex) {
            debugListeners.forEach(l -> l.failed(lazyEvent, listenerType, ex));
        }
    }

     CompletableFuture fireAny(final LazyEvent event) {
        // loan all listeners
        debugListeners.forEach(l -> l.requested(event));
        final Stream> registered = getRegisteredEventListeners(event.getEventType());
        // send the event to all listeners, execute on the background
        final Stream> withInjected = injectedDebugListener == null ? registered
                : Stream.concat(Stream.>of(injectedDebugListener), registered);
        return runAsync(withInjected.map(l -> new EventTriggerRunnable(event, l)));
    }

    private  Stream> getRegisteredEventListeners(final Class eventType) {
        List> registeredSuppliers = suppliers.computeIfAbsent(eventType,
                e -> retrieveListenerSuppliers(e));
        return registeredSuppliers.stream()
            .map(Supplier::get)
            .flatMap(Optional::stream);
    }

    public boolean addListener(final EventFiringListener listener) {
        LOGGER.debug("Adding listener {} for {}.", listener, sessionInfo);
        return debugListeners.add(listener);
    }

    public boolean removeListener(final EventFiringListener listener) {
        LOGGER.debug("Removing listener {} for {}.", listener, sessionInfo);
        return debugListeners.remove(listener);
    }

    /**
     * Purely for testing purposes.
     * 
     * @param listener
     */
    void injectEventListener(final EventListener listener) {
        this.injectedDebugListener = listener;
    }

    public CompletableFuture fire(final LazyEvent event) {
        return fireAny(event);
    }

    @SuppressWarnings("unchecked")
    public CompletableFuture fire(final SessionEvent event) {
        return fire(EventFactory.async((Class) event.getClass(), () -> event));
    }

    public boolean isListenerRegistered(final Class eventClass) {
        return getRegisteredEventListeners(eventClass).findAny()
            .isPresent();
    }

    private final class EventTriggerRunnable implements Runnable {

        private final LazyEvent event;
        private final EventListener listener;

        public EventTriggerRunnable(final LazyEvent event, final EventListener listener) {
            this.event = event;
            this.listener = listener;
        }

        @Override
        public void run() {
            var eventId = EVENT_COUNTER.getAndIncrement();
            LOGGER.debug("Starting event {} ({}).", eventId, event);
            try {
                SessionEvents.this.fireAny(event, listener);
            } finally {
                LOGGER.debug("Finished processing event {}.", eventId);
            }
        }

        @Override
        public String toString() {
            return "EventTriggerRunnable{" +
                    "event=" + event.getEventType() +
                    ", listener=" + listener.getClass() +
                    '}';
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy