/*******************************************************************************
 * Copyright (c) 2012-2015 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.api.core.notification;

import org.eclipse.che.commons.lang.cache.Cache;
import org.eclipse.che.commons.lang.cache.LoadingValueSLRUCache;
import org.eclipse.che.commons.lang.cache.SynchronizedCache;

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

import javax.inject.Singleton;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * Dispatchers events to listeners. Usage example:
 * <pre>
 *     EventService bus = new EventService();
 *     bus.subscribe(new EventSubscriber&lt;MyEvent&gt;() {
 *         &#64;Override
 *         public void onEvent(MyEvent event) {
 *             // do something with event
 *         }
 *     });
 *     bus.publish(new MyEvent());
 * </pre>
 *
 * @author andrew00x
 */
@Singleton
public class EventService {
    private static final Logger LOG = LoggerFactory.getLogger(EventService.class);

    private static final int CACHE_NUM  = 1 << 2;
    private static final int CACHE_MASK = CACHE_NUM - 1;
    private static final int SEG_SIZE   = 32;

    private final Cache<Class<?>, Set<Class<?>>>[]              typeCache;
    private final ConcurrentMap<Class<?>, Set<EventSubscriber>> subscribersByEventType;

    @SuppressWarnings("unchecked")
    public EventService() {
        subscribersByEventType = new ConcurrentHashMap<>();
        typeCache = new Cache[CACHE_NUM];
        for (int i = 0; i < CACHE_NUM; i++) {
            typeCache[i] = new SynchronizedCache<>(new LoadingValueSLRUCache<Class<?>, Set<Class<?>>>(SEG_SIZE, SEG_SIZE) {
                @Override
                protected Set<Class<?>> loadValue(Class<?> eventClass) throws RuntimeException {
                    LinkedList<Class<?>> parents = new LinkedList<>();
                    Set<Class<?>> classes = new HashSet<>();
                    parents.add(eventClass);
                    while (!parents.isEmpty()) {
                        Class<?> clazz = parents.pop();
                        classes.add(clazz);
                        Class<?> parent = clazz.getSuperclass();
                        if (parent != null) {
                            parents.add(parent);
                        }
                        Class<?>[] interfaces = clazz.getInterfaces();
                        if (interfaces.length > 0) {
                            Collections.addAll(parents, interfaces);
                        }
                    }
                    return classes;
                }
            });
        }
    }

    /**
     * Publish event {@code event}.
     *
     * @param event
     *         event
     */
    @SuppressWarnings("unchecked")
    public void publish(Object event) {
        if (event == null) {
            throw new IllegalArgumentException("Null event.");
        }
        final Class<?> eventClass = event.getClass();
        for (Class<?> clazz : typeCache[eventClass.hashCode() & CACHE_MASK].get(eventClass)) {
            final Set<EventSubscriber> eventSubscribers = subscribersByEventType.get(clazz);
            if (eventSubscribers != null && !eventSubscribers.isEmpty()) {
                for (EventSubscriber eventSubscriber : eventSubscribers) {
                    try {
                        LOG.debug("Publish event {} for {}", event, eventSubscriber);
                        eventSubscriber.onEvent(event);
                    } catch (RuntimeException e) {
                        LOG.error(e.getMessage(), e);
                    }
                }
            }
        }
    }

    /**
     * Subscribe event listener.
     *
     * @param subscriber
     *         event subscriber
     */
    public void subscribe(EventSubscriber<?> subscriber) {
        final Class<?> eventType = getEventType(subscriber);
        Set<EventSubscriber> entries = subscribersByEventType.get(eventType);
        if (entries == null) {
            Set<EventSubscriber> newEntries = new CopyOnWriteArraySet<>();
            entries = subscribersByEventType.putIfAbsent(eventType, newEntries);
            if (entries == null) {
                entries = newEntries;
            }
        }
        entries.add(subscriber);
    }

    /**
     * Unsubscribe event listener.
     *
     * @param subscriber
     *         event subscriber
     */
    public void unsubscribe(EventSubscriber<?> subscriber) {
        final Class<?> eventType = getEventType(subscriber);
        final Set<EventSubscriber> entries = subscribersByEventType.get(eventType);
        if (entries != null && !entries.isEmpty()) {
            boolean changed = entries.remove(subscriber);
            if (changed) {
                if (entries.isEmpty()) {
                    subscribersByEventType.remove(eventType);
                }
            }
        }
    }

    private Class<?> getEventType(EventSubscriber<?> subscriber) {
        Class<?> eventType = null;
        Class<?> clazz = subscriber.getClass();
        while (clazz != null && eventType == null) {
            for (Type type : clazz.getGenericInterfaces()) {
                if (type instanceof ParameterizedType) {
                    final ParameterizedType parameterizedType = (ParameterizedType)type;
                    final Type rawType = parameterizedType.getRawType();
                    if (EventSubscriber.class == rawType) {
                        final Type[] typeArguments = parameterizedType.getActualTypeArguments();
                        if (typeArguments.length == 1) {
                            if (typeArguments[0] instanceof Class) {
                                eventType = (Class)typeArguments[0];
                            }
                        }
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
        if (eventType == null) {
            throw new IllegalArgumentException(String.format("Unable determine type of events processed by %s", subscriber));
        }
        return eventType;
    }
}

@@ -4316,32 +4316,141 @@
  event listener.
+ The event to subscribe to is inferred by checking the generic type arguments of the%0A     * given subscriber.
 %0A     *%0A     * @
@@ -4618,32 +4618,590 @@
 pe(subscriber);%0A
+        doSubscribe(subscriber, eventType);%0A    %7D%0A%0A    /**%0A     * Subscribe to an event. The given subscriber will be called whenever an instance of the specified event is%0A     * published.%0A     *%0A     * @param subscriber The subscriber to call when an event is published.%0A     * @param eventType The event to subscribe to.%0A     */%0A    public %3CT%3E void subscribe(EventSubscriber%3C? extends T%3E subscriber, Class%3CT%3E eventType) %7B%0A        doSubscribe(subscriber, eventType);%0A    %7D%0A%0A    private void doSubscribe(EventSubscriber%3C?%3E subscriber, Class%3C?%3E eventType) %7B%0A
         Set%3CEven

Allow subscribing to a specific event type explicitly in EventService