package org.djutils.event;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.djutils.event.ref.Reference;
import org.djutils.event.ref.ReferenceType;
import org.djutils.event.ref.StrongReference;
import org.djutils.event.ref.WeakReference;
import org.djutils.exceptions.Throw;
import org.djutils.logger.CategoryLogger;
/**
* The EventProducer forms the reference implementation of the EventProducerInterface. Objects extending this class are provided
* all the functionalities for registration and event firing. The storage of the listeners is done in a Map with the EventType
* as the key, and a List of References (weak or strong) to the Listeners.
*
* Copyright (c) 2002-2019 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information https://djutils.org. The DJUTILS project is
* distributed under a three-clause BSD-style license, which can be found at
* https://djutils.org/docs/license.html. This class was
* originally part of the DSOL project, see
* https://simulation.tudelft.nl/dsol/manual.
*
* @author Peter Jacobs
* @author Alexander Verbraeck
*/
public abstract class EventProducer implements EventProducerInterface, Serializable
{
/** The default serial version UID for serializable classes. */
private static final long serialVersionUID = 20140830L;
/** listeners is the collection of interested listeners. */
protected EventListenerMap listeners = new EventListenerMap();
/** the semaphore used to lock while performing thread sensitive operations. */
private Object semaphore = new Object();
/** the cache to prevent continuous reflection. */
private transient static Map, EventType[]> EVENTTYPE_CACHE = new LinkedHashMap<>();
/**
* checks whether no duplicate short values are assigned to the producer. An event producer produces events of a certain
* eventType. This eventType functions as a marker for registration. If the eventProducer defines two non-private, static
* EventTypes with an equal value, the marker function is lost. This method checks for this particular problem. Note that
* this will also return true when a superclass defines the same non-private, static EventType as a subclass.
* @return returns whether every non-private, static EventType in this class is unique.
*/
private boolean checkDuplicateEventTypes()
{
EventType[] events = getStaticEventTypes(getClass());
for (int i = 0; i < events.length; i++)
{
for (int j = i + 1; j < events.length; j++) // assumes symmetric "equals"
{
if (events[i].equals(events[j]))
{
return false;
}
}
}
return true;
}
/**
* constructs a new EventProducer and checks for double values in events.
*/
public EventProducer()
{
if (!this.checkDuplicateEventTypes())
{
throw new RuntimeException("EventProducer failed: " + "more events have the same class + name combination");
}
}
/** {@inheritDoc} */
@Override
public final synchronized boolean addListener(final EventListenerInterface listener, final EventType eventType)
{
return this.addListener(listener, eventType, EventProducerInterface.FIRST_POSITION);
}
/** {@inheritDoc} */
@Override
public final synchronized boolean addListener(final EventListenerInterface listener, final EventType eventType,
final ReferenceType referenceType)
{
return this.addListener(listener, eventType, EventProducerInterface.FIRST_POSITION, referenceType);
}
/** {@inheritDoc} */
@Override
public final synchronized boolean addListener(final EventListenerInterface listener, final EventType eventType,
final int position)
{
return this.addListener(listener, eventType, position, ReferenceType.STRONG);
}
/** {@inheritDoc} */
@Override
public final synchronized boolean addListener(final EventListenerInterface listener, final EventType eventType,
final int position, final ReferenceType referenceType)
{
Throw.whenNull(eventType, "eventType cannot be null");
Throw.whenNull(referenceType, "referenceType cannot be null");
if (listener == null || position < EventProducerInterface.LAST_POSITION)
{
return false;
}
synchronized (this.semaphore)
{
Reference reference = null;
if (referenceType.isStrong())
{
reference = new StrongReference(listener);
}
else
{
reference = new WeakReference(listener);
}
if (this.listeners.containsKey(eventType))
{
for (Reference entry : this.listeners.get(eventType))
{
if (listener.equals(entry.get()))
{
return false;
}
}
List> entries = this.listeners.get(eventType);
if (position == EventProducerInterface.LAST_POSITION)
{
entries.add(reference);
}
else
{
entries.add(position, reference);
}
}
else
{
List> entries = new ArrayList<>();
entries.add(reference);
this.listeners.put(eventType, entries);
}
}
return true;
}
/**
* fires the event to the listener. This method is a hook method. The default implementation simply invokes the notify on
* the listener. In specific cases (filtering, storing, queuing, this method can be overwritten.
* @param listener EventListenerInterface; the listener for this event
* @param event EventInterface; the event to fire
* @return the event
* @throws RemoteException on network failure.
*/
protected synchronized EventInterface fireEvent(final EventListenerInterface listener, final EventInterface event)
throws RemoteException
{
listener.notify(event);
return event;
}
/**
* fires an event to subscribed listeners.
* @param event EventInterface; the event.
* @return the event.
*/
protected synchronized EventInterface fireEvent(final EventInterface event)
{
if (this.listeners.containsKey(event.getType()))
{
synchronized (this.semaphore)
{
// make a safe copy because of possible removeListener() in notify() method during fireEvent
List> listenerList = new ArrayList<>(this.listeners.get(event.getType()));
for (Reference reference : listenerList)
{
EventListenerInterface listener = reference.get();
try
{
if (listener != null)
{
// The garbage collection has not cleaned the referent
this.fireEvent(listener, event);
}
else
{
// The garbage collection cleaned the referent;
// there is no need to keep the subscription
this.removeListener(reference, event.getType());
}
}
catch (RemoteException remoteException)
{
// A network failure prevented the delivery,
// subscription is removed.
this.removeListener(reference, event.getType());
}
}
}
}
return event;
}
/**
* fires a value to listeners subscribed to eventType.
* @param eventType EventType; the eventType of the event.
* @param value Object; the value of the event.
* @return the Serializable value.
*/
protected synchronized Object fireEvent(final EventType eventType, final Object value)
{
this.fireEvent(new Event(eventType, this, value));
return value;
}
/**
* notifies listeners subscribed to eventType.
* @param eventType EventType; the eventType of the event.
*/
protected synchronized void fireEvent(final EventType eventType)
{
this.fireEvent(new Event(eventType, this, null));
}
/**
* fires a Serializable value to listeners subscribed to eventType. A timed event is fired.
* @param eventType EventType; the eventType of the event.
* @param value Object; the value of the event.
* @param time C; a timestamp for the event.
* @return the Serializable value.
* @param the comparable type to indicate the time when the event is fired.
*/
protected synchronized > Object fireTimedEvent(final EventType eventType, final Object value,
final C time)
{
this.fireEvent(new TimedEvent(eventType, this, value, time));
return value;
}
/**
* fires a byte value to listeners subscribed to eventType.
* @param eventType EventType; the eventType of the event.
* @param value byte; the value of the event.
* @return the byte value.
*/
protected synchronized byte fireEvent(final EventType eventType, final byte value)
{
this.fireEvent(eventType, Byte.valueOf(value));
return value;
}
/**
* fires a byte value to listeners subscribed to eventType. A timed event is fired.
* @param eventType EventType; the eventType of the event.
* @param value byte; the value of the event.
* @param time C; a timestamp for the event.
* @param the comparable type to indicate the time when the event is fired.
* @return the byte value.
*/
protected synchronized > byte fireTimedEvent(final EventType eventType, final byte value,
final C time)
{
this.fireTimedEvent(eventType, Byte.valueOf(value), time);
return value;
}
/**
* fires a boolean value to listeners subscribed to eventType.
* @param eventType EventType; the eventType of the event.
* @param value boolean; the value of the event.
* @return the boolean value.
*/
protected synchronized boolean fireEvent(final EventType eventType, final boolean value)
{
this.fireEvent(eventType, Boolean.valueOf(value));
return value;
}
/**
* fires a boolean value to listeners subscribed to eventType. A timed event is fired.
* @param eventType EventType; the eventType of the event.
* @param value boolean; the value of the event.
* @param time C; a timestamp for the event.
* @param the comparable type to indicate the time when the event is fired.
* @return the boolean value.
*/
protected synchronized > boolean fireTimedEvent(final EventType eventType, final boolean value,
final C time)
{
this.fireTimedEvent(eventType, Boolean.valueOf(value), time);
return value;
}
/**
* fires a double value to listeners subscribed to eventType.
* @param eventType EventType; the eventType of the event.
* @param value double; the value of the event.
* @return the double value.
*/
protected synchronized double fireEvent(final EventType eventType, final double value)
{
this.fireEvent(eventType, Double.valueOf(value));
return value;
}
/**
* fires a double value to listeners subscribed to eventType. A timed event is fired.
* @param eventType EventType; the eventType of the event.
* @param value double; the value of the event.
* @param time C; a timestamp for the event.
* @param the comparable type to indicate the time when the event is fired.
* @return the double value.
*/
protected synchronized > double fireTimedEvent(final EventType eventType, final double value,
final C time)
{
this.fireTimedEvent(eventType, Double.valueOf(value), time);
return value;
}
/**
* fires an integer value to listeners subscribed to eventType.
* @param eventType EventType; the eventType of the event.
* @param value int; the value of the event.
* @return the integer value.
*/
protected synchronized int fireEvent(final EventType eventType, final int value)
{
this.fireEvent(eventType, Integer.valueOf(value));
return value;
}
/**
* fires an integer value to listeners subscribed to eventType. A timed event is fired.
* @param eventType EventType; the eventType of the event.
* @param value int; the value of the event.
* @param time C; a timestamp for the event.
* @param the comparable type to indicate the time when the event is fired.
* @return the integer value.
*/
protected synchronized > int fireTimedEvent(final EventType eventType, final int value,
final C time)
{
this.fireTimedEvent(eventType, Integer.valueOf(value), time);
return value;
}
/**
* fires a long value to listeners subscribed to eventType.
* @param eventType EventType; the eventType of the event.
* @param value long; the value of the event.
* @return the long value.
*/
protected synchronized long fireEvent(final EventType eventType, final long value)
{
this.fireEvent(eventType, Long.valueOf(value));
return value;
}
/**
* fires a long value to listeners subscribed to eventType. A timed event is fired.
* @param eventType EventType; the eventType of the event.
* @param value long; the value of the event.
* @param time C; a timestamp for the event.
* @param the comparable type to indicate the time when the event is fired.
* @return the long value.
*/
protected synchronized > long fireTimedEvent(final EventType eventType, final long value,
final C time)
{
this.fireTimedEvent(eventType, Long.valueOf(value), time);
return value;
}
/**
* fires a short value to listeners subscribed to eventType.
* @param eventType EventType; the eventType of the event.
* @param value short; the value of the event.
* @return the short value.
*/
protected synchronized short fireEvent(final EventType eventType, final short value)
{
this.fireEvent(eventType, Short.valueOf(value));
return value;
}
/**
* fires a short value to listeners subscribed to eventType. A timed event is fired.
* @param eventType EventType; the eventType of the event.
* @param value short; the value of the event.
* @param time C; a timestamp for the event.
* @param the comparable type to indicate the time when the event is fired.
* @return the short value.
*/
protected synchronized > short fireTimedEvent(final EventType eventType, final short value,
final C time)
{
this.fireTimedEvent(eventType, Short.valueOf(value), time);
return value;
}
/**
* Return the non-private, static EventType fields for the provided class, and add them to the EventProducer cache.
* @param checkClass Class<? extends EventProducer>; the class to return the non-private, static EventTypes for
* @return EventType[]; the non-private, static event types of this EventProducer
*/
private static synchronized EventType[] getStaticEventTypes(final Class extends EventProducer> checkClass)
{
if (EVENTTYPE_CACHE.containsKey(checkClass))
{
return EVENTTYPE_CACHE.get(checkClass);
}
List fieldList = new ArrayList();
Class> clazz = checkClass;
while (clazz != null)
{
Field[] declaredFields = clazz.getDeclaredFields();
for (int i = 0; i < declaredFields.length; i++)
{
fieldList.add(declaredFields[i]);
}
clazz = clazz.getSuperclass();
}
List eventTypeList = new ArrayList<>();
for (Field field : fieldList)
{
if (field.getType().equals(EventType.class) && !Modifier.isPrivate(field.getModifiers())
&& Modifier.isStatic(field.getModifiers()))
{
try
{
eventTypeList.add((EventType) field.get(null)); // only static fields
}
catch (Exception exception)
{
// should not happen
CategoryLogger.always().error(exception);
}
}
}
EVENTTYPE_CACHE.put(checkClass, eventTypeList.toArray(new EventType[eventTypeList.size()]));
return EVENTTYPE_CACHE.get(checkClass);
}
/**
* removes all the listeners from the producer.
* @return the number of removed listeners. XXX : not...
*/
protected synchronized int removeAllListeners()
{
int result = this.listeners.size();
this.listeners = null;
this.listeners = new EventListenerMap();
return result;
}
/**
* removes all the listeners of a class.
* @param ofClass Class<?>; the class or superclass.
* @return the number of listeners which were removed.
*/
protected synchronized int removeAllListeners(final Class> ofClass)
{
int result = 0;
Map> removeMap = new LinkedHashMap<>();
synchronized (this.semaphore)
{
for (EventType type : this.listeners.keySet())
{
for (Iterator> ii = this.listeners.get(type).iterator(); ii.hasNext();)
{
Reference listener = ii.next();
if (listener.get().getClass().isAssignableFrom(ofClass))
{
removeMap.put(type, listener);
result++;
}
}
}
}
for (EventType type : removeMap.keySet())
{
removeListener(removeMap.get(type).get(), type);
}
return result;
}
/** {@inheritDoc} */
@Override
public final synchronized boolean removeListener(final EventListenerInterface listener, final EventType eventType)
{
if (!this.listeners.containsKey(eventType))
{
return false;
}
boolean result = false;
synchronized (this.semaphore)
{
for (Iterator> i = this.listeners.get(eventType).iterator(); i.hasNext();)
{
Reference reference = i.next();
EventListenerInterface entry = reference.get();
if (entry == null)
{
i.remove();
}
else
{
if (listener.equals(entry))
{
i.remove();
result = true;
}
}
}
if (this.listeners.get(eventType).size() == 0)
{
this.listeners.remove(eventType);
}
}
return result;
}
/**
* removes a reference from the subscription list.
* @param reference Reference<EventListenerInterface>; the reference to remove
* @param eventType EventType; the eventType for which reference must be removed
* @return success whenever the reference is removes; otherwise returns false.
*/
private synchronized boolean removeListener(final Reference reference, final EventType eventType)
{
boolean success = false;
for (Iterator> i = this.listeners.get(eventType).iterator(); i.hasNext();)
{
if (i.next().equals(reference))
{
i.remove();
success = true;
}
}
if (this.listeners.get(eventType).size() == 0)
{
this.listeners.remove(eventType);
}
return success;
}
/** {@inheritDoc} */
@Override
public boolean hasListeners()
{
return !this.listeners.isEmpty();
}
/** {@inheritDoc} */
@Override
public int numberOfListeners(final EventType eventType)
{
if (this.listeners.containsKey(eventType))
return this.listeners.get(eventType).size();
return 0;
}
/** {@inheritDoc} */
@Override
public Set getEventTypesWithListeners()
{
return this.listeners.keySet(); // is already a safe copy
}
}