peholmst / vaadin4spring

Vaadin integration for Spring and Spring Boot
Apache License 2.0
263 stars 131 forks source link

Eventbus - allow custom scopes in eventbus addon #289

Open florianschmitt opened 8 years ago

florianschmitt commented 8 years ago

Hi, thanks for the great add on. I'm using the eventbus addon and stumbled upon an issue. I had the need for modal windows in my application, since I didn't want a view-change in that case, I created my own custom scope SingleModalWindowScope. I'm still testing, but it seems to do the job. My problem is now, that I reuse views, in the modal window, which use the ViewEventBus. Now if a event is fired, possibly both, the view in the background and the modal window are listing to it. I checked the eventbus code and I guess I could implement my own ScopedEventBus, but I wouldn't be able to use it with the current API because of the EventScope enum, which only allows the predefined scopes. Do you see any problems with e.g. giving the EventScope enum a field for the scope-name and allowing <T> void publish(EventScope scope, String topic, Object sender, T payload) throws UnsupportedOperationException;

to overload for instance a newly introduced method

<T> void publish(String scopeName, String topic, Object sender, T payload) throws UnsupportedOperationException;

I didn't analyze the whole code, do you see any other problems with allowing custom scopes in the eventbus?

Thanks!

BR Florian

florianschmitt commented 8 years ago

Hi again,

I needed a solution, so I put together a hack. I implemented a ViewEventBus, which is pausable. With this, I can set all registered listeners to paused when I open a modal window and set them to play again, when the window is closed... seems a bit dirty, but it does the job for now. Here is my implementation if anyone has a similar problem:

PausableViewEventBus.java

package org.vaadin.spring.events.internal;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
import org.vaadin.spring.events.EventBus.ViewEventBus;
import org.vaadin.spring.events.EventScope;
import org.vaadin.spring.events.annotation.EventBusListenerMethod;
import org.vaadin.spring.events.internal.ListenerCollection.Listener;
import org.vaadin.spring.util.ClassUtils;

/**
 * This class allows pausing subscribed listeners (used for allowing modal
 * windows which use the same events as an opened view).
 * 
 * WARNING: currently only listeners, subscribed with @EventBusListenerMethod
 * are paused, NO EventBusListener implementations.
 * 
 * @author fschmitt
 *
 */
public class PausableViewEventBus extends ScopedEventBus implements ViewEventBus {

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

    public PausableViewEventBus(UIEventBus parentEventBus) {
        super(EventScope.VIEW, parentEventBus);
    }

    /**
     * Switches MethodListenerWrapper to PausableMethodListenerWrapper
     * implementation to allow pausing.
     */
    @Override
    public void subscribe(Object listener, boolean includingPropagatingEvents) {
        logger.trace("Subscribing listener [{}] to event bus [{}], includingPropagatingEvents = {}", listener, this,
                includingPropagatingEvents);

        final int[] foundMethods = new int[1];
        ClassUtils.visitClassHierarchy(new ClassUtils.ClassVisitor() {
            @Override
            public void visit(Class<?> clazz) {
                for (Method m : clazz.getDeclaredMethods()) {
                    if (m.isAnnotationPresent(EventBusListenerMethod.class)) {
                        if (m.getParameterTypes().length == 1) {
                            logger.trace("Found listener method [{}] in listener [{}]", m.getName(), listener);
                            MethodListenerWrapper l = new PausableMethodListenerWrapper(PausableViewEventBus.this,
                                    listener, includingPropagatingEvents, m);
                            accessListenersField().add(l);
                            foundMethods[0]++;
                        } else {
                            throw new IllegalArgumentException(
                                    "Listener method " + m.getName() + " does not have the required signature");
                        }
                    }
                }
            }
        }, listener.getClass());

        if (foundMethods[0] == 0) {
            logger.warn("Listener [{}] did not contain a single listener method!", listener);
        }
    }

    /**
     * Pauses or unpauses all currently subscribed MethodListeners.
     * 
     * @param value
     *            pause or unpause
     */
    public void setAllListenersPaused(boolean value) {
        Set<Listener> listenersFromListenerCollection = accessListenersFieldFromListenerCollection();
        listenersFromListenerCollection.stream()//
                .filter(l -> PausableMethodListenerWrapper.class.isAssignableFrom(l.getClass()))//
                .map(PausableMethodListenerWrapper.class::cast)//
                .forEach(l -> l.setPaused(value));
    }

    private Set<Listener> accessListenersFieldFromListenerCollection() {
        Field listenersField = ReflectionUtils.findField(ListenerCollection.class, "listeners", Set.class);
        listenersField.setAccessible(true);
        try {
            @SuppressWarnings({ "unchecked", "rawtypes" })
            Set<Listener> result = (Set) listenersField.get(accessListenersField());
            return result;
        } catch (IllegalArgumentException | IllegalAccessException e) {
            ReflectionUtils.handleReflectionException(e);
            return null;
        }

    }

    private ListenerCollection accessListenersField() {
        Field listenersField = ReflectionUtils.findField(getClass(), "listeners", ListenerCollection.class);
        listenersField.setAccessible(true);
        try {
            ListenerCollection result = (ListenerCollection) listenersField.get(this);
            return result;
        } catch (IllegalArgumentException | IllegalAccessException e) {
            ReflectionUtils.handleReflectionException(e);
            return null;
        }
    }
}

PausableMethodListenerWrapper.java

package org.vaadin.spring.events.internal;

import java.lang.reflect.Method;

import org.vaadin.spring.events.Event;
import org.vaadin.spring.events.EventBus;

import lombok.Setter;

public class PausableMethodListenerWrapper extends MethodListenerWrapper {

    public PausableMethodListenerWrapper(EventBus owningEventBus, Object listenerTarget,
            boolean includingPropagatingEvents, Method listenerMethod) {
        super(owningEventBus, listenerTarget, includingPropagatingEvents, listenerMethod);
    }

    @Setter
    private boolean paused;

    @Override
    public boolean supports(Event<?> event) {
        if (paused)
            return false;
        return super.supports(event);
    }
}

PausableMethodListenerWrapper.java

package com.explicatis.system.scope;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.vaadin.spring.events.EventBus;
import org.vaadin.spring.events.internal.PausableViewEventBus;

import com.vaadin.spring.internal.ViewScopeImpl;

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class EventBusConfigurationForPausableViewEventBus {

    @Autowired
    @Lazy
    private EventBus.UIEventBus uiEventBus;

    @Bean
    @Scope(value = ViewScopeImpl.VAADIN_VIEW_SCOPE_NAME, proxyMode = ScopedProxyMode.NO)
    @Primary
    EventBus.ViewEventBus viewEventBus() {
        return new PausableViewEventBus(uiEventBus);
    }
}
peholmst commented 8 years ago

Thanks for your input. I'll have a look at what could be done to support other scopes in the future. Unfortunately I've been busy with other projects so it might take a while.