package org.marketcetera.photon.commons.ui.databinding; import java.text.MessageFormat; import java.util.Collection; import java.util.Set; import org.eclipse.core.databinding.ObservablesManager; import org.eclipse.core.databinding.observable.IObservable; import org.eclipse.core.databinding.observable.Realm; import org.eclipse.core.databinding.observable.map.IMapChangeListener; import org.eclipse.core.databinding.observable.map.IObservableMap; import org.eclipse.core.databinding.observable.map.MapChangeEvent; import org.eclipse.core.databinding.observable.set.IObservableSet; import org.eclipse.core.databinding.property.value.IValueProperty; import org.marketcetera.photon.commons.Validate; import org.marketcetera.util.misc.ClassVersion; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; /* $License$ */ /** * Tracks changes to value properties on objects. An instance is initialized * with a static list of {@link IValueProperty} that define the properties to * watch. When an {@link IObservableSet} is {@link #watch(IObservableSet) * watched}, the {@link IPropertiesChangedListener} supplied in the constructor * will be notified via * {@link IPropertiesChangedListener#propertiesChanged(ImmutableSet)} every time * one or more of the properties changes for items in the set. * <p> * Currently this only supports value properties and not list properties. * <p> * This class is thread safe. {@link #watch(IObservableSet)} and * {@link #dispose()} can only be called from a single thread that has a default * realm while that realm is {@link Realm#isCurrent() current}. The methods will * throw an exception otherwise. * * @author <a href="mailto:will@marketcetera.com">Will Horn</a> * @version $Id: PropertyWatcher.java 16154 2012-07-14 16:34:05Z colin $ * @since 2.0.0 */ @ClassVersion("$Id: PropertyWatcher.java 16154 2012-07-14 16:34:05Z colin $") public final class PropertyWatcher { /** * Interface for listeners that receive notification when properties change. */ @ClassVersion("$Id: PropertyWatcher.java 16154 2012-07-14 16:34:05Z colin $") public interface IPropertiesChangedListener { /** * Notifies the listener that properties have changed. * * @param affectedElements * the elements whose properties have changed */ void propertiesChanged(ImmutableSet<?> affectedElements); } /** * Captures the realm to which this object is confined. Thread safety * provided by thread confinement. */ private Realm mRealm; /** * Manages observables that need to be disposed with this object. Thread * safety provided by thread confinement. */ private ObservablesManager mObservables; private final ImmutableList<IValueProperty> mProperties; private final IPropertiesChangedListener mListener; private final IMapChangeListener mMapChangeListener = new IMapChangeListener() { public void handleMapChange(MapChangeEvent event) { Set<?> affectedElements = event.diff.getChangedKeys(); if (!affectedElements.isEmpty()) { mListener.propertiesChanged(ImmutableSet .copyOf(affectedElements)); } } }; private boolean mDisposed; /** * Constructor. * * @param properties * properties to watch * @param listener * the listener that will be notified when watched properties * change * @throws IllegalArgumentException * if properties is null, empty, or has null elements, or if * listener is null */ public PropertyWatcher(Collection<? extends IValueProperty> properties, IPropertiesChangedListener listener) { Validate.nonNullElements(properties, "properties"); //$NON-NLS-1$ Validate.notNull(listener, "listener"); //$NON-NLS-1$ mProperties = ImmutableList.copyOf(properties); mListener = listener; } /** * Tracks the provided set of elements. Tracking will not stop until this * PropertyWatcher is {@link #dispose() disposed}. * <p> * This can be called multiple times. If called multiple times with sets * that overlap, the listener might be notified multiple times when changes * occur. * <p> * Although not validated, each element in the dynamic elements set is * expected to be non-null and an {@link IObservable} that supports the * properties being watched. Such observables should also be on the default * realm. * * @param elements * elements to track * @throws IllegalStateException * if called from a thread without a default realm * @throws IllegalStateException * if called when the default realm is not current * @throws IllegalStateException * if called multiple times from different threads * @throws IllegalStateException * if the PropertyWatcher has been disposed * @throws IllegalArgumentException * if elements is null or is not on the default realm */ public synchronized void watch(IObservableSet elements) { Realm realm = Realm.getDefault(); if (realm == null) { throw new IllegalStateException( "must be called from a thread with a default realm"); //$NON-NLS-1$ } if (!realm.isCurrent()) { throw new IllegalStateException( "must be called from the default realm"); //$NON-NLS-1$ } if (mRealm != realm) { if (mRealm == null) { mRealm = realm; mObservables = new ObservablesManager(); } else { throw new IllegalStateException(MessageFormat.format( "called from invalid realm [0], expected [1]", //$NON-NLS-1$ realm, mRealm)); } } if (mDisposed) { throw new IllegalStateException( "PropertyWatcher has already been disposed"); //$NON-NLS-1$ } Validate.notNull(elements, "elements"); //$NON-NLS-1$ Realm elementsRealm = elements.getRealm(); if (elementsRealm != mRealm) { throw new IllegalArgumentException(MessageFormat.format( "elements is on an invalid realm [0], expected [1]", //$NON-NLS-1$ elementsRealm, mRealm)); } for (IValueProperty property : mProperties) { IObservableMap map = property.observeDetail(elements); map.addMapChangeListener(mMapChangeListener); mObservables.addObservable(map); } } /** * Disposes this PropertyWatcher, after which it must no longer be used. */ public synchronized void dispose() { if (!mDisposed) { mDisposed = true; if (mRealm != null) { // dispose on the realm so dispose listeners fire on the realm mRealm.exec(new Runnable() { @Override public void run() { mObservables.dispose(); } }); } } } }