package org.archstudio.bna.logics.events; import java.util.Set; import org.archstudio.bna.BNAModelEvent; import org.archstudio.bna.IBNAModel; import org.archstudio.bna.IBNAModelListener; import org.archstudio.bna.IBNAWorld; import org.archstudio.bna.IThing; import org.archstudio.bna.facets.IHasWorld; import org.archstudio.bna.keys.IThingKey; import org.archstudio.bna.keys.ThingKey; import org.archstudio.bna.logics.AbstractThingLogic; import org.archstudio.bna.logics.tracking.ThingValueTrackingLogic; import org.archstudio.bna.things.utility.NoThing; import com.google.common.base.Preconditions; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.collect.Sets; /** * Monitors worlds within {@link IHasWorld things with worlds} for changes. Performs two actions when a change happens * in the monitored worlds: * <ul> * <li>Forwards the event to all logics implementing {@link IInternalBNAModelListener}.</li> * <li>Updates a {@link ListenToSubWorldEventsLogic#WORLD_CHANGED_KEY value} of the {@link #worldChangeNotifierThing * world change notifier thing} causing a change event in the parent world.</li> * </ul> * The world change notifier thing is simply used to create a new change event in the local model. This causes listeners * to treat the world as if modified even though the original source of the event was from a world in a thing. This is * used, among other things, to force a redraw of the top level view. * <p/> * Care is taken to prevent endless updates in models that have cyclic world references. Instead, each impacted world * will receive only one notification. * * @author sahendrickson@gmail.com (Scott A. Hendrickson) */ public class ListenToSubWorldEventsLogic extends AbstractThingLogic implements IBNAModelListener { /** The key containing the change event that is generated when a {@link IHasWorld monitored world} is modified. */ private static final IThingKey<WorldChange> WORLD_CHANGED_KEY = ThingKey.create(ListenToSubWorldEventsLogic.class); /** * The value set in the {@link #getWorldChangedNotifierThing(IBNAWorld) special thing} in the model, which indicates * that a thing's {@link IHasWorld world} world has been modified. This value is special in that this logic updates * it with the worlds that have already been notified in order to handle cycles of world references in the BNA * model. */ private static final class WorldChange { /** * The worlds that have already been notified of a change to another thing's {@link IHasWorld world}. This is * updated by the logic and then propagated to the {@link #getWorldChangedNotifierThing(IBNAWorld) special * thing} in this world if the world has not already received the event. */ private final Set<IBNAWorld> worldsNotified; /** * Creates a change value with a record that the current world has already received an event for the change * (i.e., the current world has already received a {@link IBNAModelListener} event). * * @param world The world originating the change. */ public WorldChange(IBNAWorld world) { this.worldsNotified = Sets.newHashSet(world); } } /** * Returns the {@link NoThing special thing} in the world that is used to propagate changes from other thing's * {@link IHasWorld world} events. * * @param world The world form which to get the special thing. * @return */ private static final IThing getWorldChangedNotifierThing(IBNAWorld world) { IBNAModel model = world.getBNAModel(); IThing worldChangedTickerThing = model.getThing(ListenToSubWorldEventsLogic.class); if (worldChangedTickerThing == null) { worldChangedTickerThing = model.addThing(new NoThing(ListenToSubWorldEventsLogic.class)); } return worldChangedTickerThing; } /** * A {@link IBNAModelListener BNA model listener} that listens for events in a specific {@link #monitoredWorld * monitored world}. */ private class WorldChangeMonitor implements IBNAModelListener { /** The world that multiple things refer to. */ private final IBNAWorld monitoredWorld; /** * The world change notifier thing in the {@link #reportingToWorld reporting world} that is modified when the * {@link #monitoredWorld monitored world} is changed. */ private final IThing worldChangeNotifierThing; /** * Creates a new {@link WorldChangeMonitor} for the given world. * * @param monitoredWorld The world to be monitored for changes. */ public WorldChangeMonitor(IBNAWorld monitoredWorld) { this.monitoredWorld = Preconditions.checkNotNull(monitoredWorld); this.worldChangeNotifierThing = getWorldChangedNotifierThing(world); monitoredWorld.getBNAModel().addBNAModelListener(this); } /** * Disposes this monitor by removing it from the list of listeners in the {@link #monitoredWorld monitored * world}. */ public void dispose() { monitoredWorld.getBNAModel().removeBNAModelListener(this); } /** * Performs two actions when a change in the {@link #monitoredWorld monitored world} happens: * <ul> * <li>Forwards the event to all logics implementing {@link IInternalBNAModelListener} in the * {@link #reportingToWorld}.</li> * <li>Updates a {@link ListenToSubWorldEventsLogic#WORLD_CHANGED_KEY value} of the * {@link #worldChangeNotifierThing world change notifier thing} causing a change event in the parent world. * </li> * </ul> * However, if a change has already caused the world change notifier thing to be updated it is not updated a * again. This prevents cycles in the {@link IHasWorld} graph from causing endless notifications. */ @Override public void bnaModelChanged(BNAModelEvent evt) { if (evt.getThingEvent() != null && evt.getThingEvent().getNewPropertyValue() instanceof WorldChange) { WorldChange worldChange = (WorldChange) evt.getThingEvent().getNewPropertyValue(); if (worldChange.worldsNotified.add(world)) { worldChangeNotifierThing.set(WORLD_CHANGED_KEY, worldChange); } } else { for (IHasWorld thingWithMonitoredWorld : valueLogic.getThings(IHasWorld.WORLD_KEY, monitoredWorld, IHasWorld.class)) { for (IInternalBNAModelListener l : logics.getThingLogics(IInternalBNAModelListener.class)) { l.internalBNAModelChanged(thingWithMonitoredWorld, evt); } } worldChangeNotifierThing.set(WORLD_CHANGED_KEY, new WorldChange(world)); } } } /** * The value logic used to search for things that reference the monitored world. These things are used when * informing logics implementing {@link IInternalBNAModelListener}. */ protected final ThingValueTrackingLogic valueLogic; /** The monitors for each monitored world. */ private LoadingCache<IBNAWorld, WorldChangeMonitor> worldMonitors = CacheBuilder.newBuilder().weakKeys().removalListener(new RemovalListener<IBNAWorld, WorldChangeMonitor>() { @Override public void onRemoval(RemovalNotification<IBNAWorld, WorldChangeMonitor> notification) { notification.getValue().dispose(); } }).build(new CacheLoader<IBNAWorld, WorldChangeMonitor>() { @Override public WorldChangeMonitor load(IBNAWorld key) throws Exception { return new WorldChangeMonitor(key); } }); /** * Creates a new instance of this logic for the specified world. * * @param world The world that this logic will be a part of. */ public ListenToSubWorldEventsLogic(IBNAWorld world) { super(world); valueLogic = world.getThingLogicManager().addThingLogic(ThingValueTrackingLogic.class); for (IThing thing : model.getAllThings()) { if (thing instanceof IHasWorld) { IBNAWorld thingWorld = ((IHasWorld) thing).getWorld(); if (thingWorld != null) { worldMonitors.getUnchecked(thingWorld); } } } } /** * Adds things with a world to the list of worlds that are monitored. * * @param evt The BNA model event. */ @Override public void bnaModelChanged(BNAModelEvent evt) { // If a thing has a world set then create a WorldChangeMonitor to list to its events. if (evt.getThingEvent() != null && evt.getTargetThing() instanceof IHasWorld) { if (IHasWorld.WORLD_KEY.equals(evt.getThingEvent().getPropertyName())) { IBNAWorld world = (IBNAWorld) evt.getThingEvent().getNewPropertyValue(); if (world != null) { worldMonitors.getUnchecked(world); } } } } }