/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Intersect Software Corporation This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.control; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSMutableArray; /** * EOObserverCenter is a static singleton-like class that manages EOObservings * that want to be notified of changes to those objects that call * notifyObserversObjectWillChange() before their properties change.
*
* * Implementation note: Because Java implements the Observer pattern in * java.util.Observable, this class knows how to register itself as an Observer * with Observables as well. However, users should note that Observables only * notify their Observers of changes after their property has changed. * EODelayedObservers would see no difference because they always receive their * notification after all changes have taken place.
*
* * This implementation uses weak references for observers and observables. The * advantage to this approach is that you do not need to explicitly unregister * observers or observables; they will be unregistered when they are * garbage-collected. Note that you will need to retain a reference to any * objects you register or they may become unregistered if no other object * references them. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 894 $ */ public class EOObserverCenter implements Observer { /** * Would much rather use a WeakHashMap, but that class compares by value, and we * need to compare by reference. This means we need to recreate a weak hashmap * with the ReferenceKey class below. Using hashtable for thread safety. */ private static Map observableToObservers = new Hashtable(); private static List omniscients = new LinkedList(); // suppression count private static int suppressions = 0; // singleton instance - needed for Observer private static EOObserverCenter instance = null; // optimization: remember last request and result private static Object lastRequest; private static NSArray lastResult; /** * Registers the specified EOObserving for notifications from the specified * object. An EOObserving can only be registered once for a given object. */ public static void addObserver(EOObserving anObserver, Object anObject) { // atomic operation synchronized (instance()) { // find observer list List observers = (List) observableToObservers.get(new ReferenceKey(anObject)); // if observer list not found, create and add item if (observers == null) { observers = new ArrayList(); observers.add(new WeakReference(anObserver)); processQueue(); observableToObservers.put(new ReferenceKey(anObject, queue), observers); // support for java.util.Observable if (anObject instanceof Observable) { ((Observable) anObject).addObserver(instance()); } } else // observer list found - scan for observer if (indexOf(observers, anObserver) < 0) { // observer not found, register it observers.add(new WeakReference(anObserver)); } lastRequest = null; lastResult = null; } } /** * Registers the specified EOObserving for notifications on all object changes. * An EOObserving can be registered as an omniscient observer at most once. Use * omniscient observers with caution. */ public static void addOmniscientObserver(EOObserving anObserver) { if (indexOf(omniscients, anObserver) < 0) { omniscients.add(anObserver); } } /** * Notifies all EOObservings registered for notifications for the specified * object. This method is typically called by objects that wish to broadcast a * notification before a property change takes place, passing itself as the * argument. */ public static void notifyObserversObjectWillChange(Object anObject) { if (observerNotificationSuppressCount() == 0) { List observers = observersForObject(anObject); EOObserving o; Iterator it = observers.iterator(); while (it.hasNext()) { o = (EOObserving) it.next(); o.objectWillChange(anObject); } } } /** * Returns an observer that is an instance of the specified class and that is * registered for notifications about the specified object. If more than one * such observer exists, which observer is returned is undetermined - use * observersForObject instead. If no observer exists, returns null. */ public static EOObserving observerForObject(Object anObject, Class aClass) { List result = observersForObject(anObject); if (result.size() == 0) return null; Object o; Iterator it = result.iterator(); while (it.hasNext()) { o = it.next(); if (aClass.isAssignableFrom(o.getClass())) { return (EOObserving) o; } } return null; } /** * Returns the number of times that notifications have been suppressed. This is * also the number of times that enableObserverNotification must be called to * allow notifications to take place. */ public static int observerNotificationSuppressCount() { return suppressions; } /** * Returns a List of observers for the specified object. Returns an empty List * if no observer has registered for that object. */ public static NSArray observersForObject(Object anObject) { synchronized (instance()) { // optimization: this is called very frequently // from the same object calling willChange() a // number of times in a row as it is updating. if (lastRequest == anObject) { return lastResult; } NSArray result; List references = observerListForObject(anObject); if (references == null) { result = NSArray.EmptyArray; } else { result = new NSMutableArray(); Object observer; Iterator it = references.iterator(); while (it.hasNext()) { observer = ((Reference) it.next()).get(); if (observer != null) { result.add(observer); } else // reference has expired { processQueue(); it.remove(); // remove from list // if last observer, unregister observable if (references.size() == 0) { observableToObservers.remove(new ReferenceKey(anObject)); } } } } lastRequest = anObject; lastResult = result; return result; } } /** * Returns a reference to the actual list of observers for the given object, or * null if it doesn't exist. */ private static List observerListForObject(Object anObject) { return (List) observableToObservers.get(new ReferenceKey(anObject)); } /** * Unregisters the specified observer for notifications from the specified * object. */ public static void removeObserver(EOObserving anObserver, Object anObject) { // atomic operation synchronized (instance()) { lastRequest = null; lastResult = null; List result = observerListForObject(anObject); if (result == null) return; int index = indexOf(result, anObserver); if (index == -1) return; // remove observer from list result.remove(index); // if last observer, unregister observable if (result.size() == 0) { processQueue(); observableToObservers.remove(new ReferenceKey(anObject)); } } } /** * Unregisters the specified omniscient observer. */ public static void removeOmniscientObserver(EOObserving anObserver) { int index = indexOf(omniscients, anObserver); if (index != -1) { omniscients.remove(index); } } /** * Enables notifications after they have been suppressed by * suppressObserverNotification. If notifications have been suppressed multiple * times, this method must be called an equal number of times to resume * notifications. If notifications are not currently suppressed, this method * does nothing. */ public static void enableObserverNotification() { if (suppressions > 0) suppressions--; } /** * Causes notifications to be suppressed until the next matching call to * enableObserverNotification. If this method is called more than once, * enableObserverNotification must be called an equal number of times for * notifications to resume. This method always causes notifications to cease * immediately. */ public static void suppressObserverNotification() { suppressions++; } /** * Because we're comparing by reference, we need to test for the existence of * the object directly. * * @return the index or -1 if not found. */ private static int indexOf(List aList, Object anObject) { if (anObject == null) return -1; synchronized (aList) { int len = aList.size(); for (int i = 0; i < len; i++) { // compare by reference if (anObject == ((Reference) aList.get(i)).get()) { return i; } } } return -1; } /** * Private singleton instance, so we can be an observer. */ private static EOObserverCenter instance() { if (instance == null) { instance = new EOObserverCenter(); } return instance; } /** * Interface Observer */ public void update(Observable o, Object arg) { notifyObserversObjectWillChange(o); } /* Reference queue for cleared WeakKeys */ private static ReferenceQueue queue = new ReferenceQueue(); /* * Remove all invalidated entries from the map, that is, remove all entries * whose keys have been discarded. This method should be invoked once by each * public mutator in this class. We don't invoke this method in public accessors * because that can lead to surprising ConcurrentModificationExceptions. */ private static void processQueue() { synchronized (instance()) { ReferenceKey rk; while ((rk = (ReferenceKey) queue.poll()) != null) { // System.out.println( "EOObserverCenter.processQueue: removing object" ); observableToObservers.remove(rk); } } } /** * Private class used to force a hashmap to perform key comparisons by * reference. Retains a weak reference just like WeakHashMap. */ static private class ReferenceKey extends WeakReference { private int hashCode; /** * Called to create a disposable reference key, used for retrieving values from * the hashtable. */ public ReferenceKey(Object anObject) { super(anObject); hashCode = anObject.hashCode(); } /** * Called to create a reference key that will be used as a key in the hashtable, * so we need the reference queue to later remove the key from the table when * referred object is no longer in use. */ public ReferenceKey(Object anObject, ReferenceQueue aQueue) { super(anObject, aQueue); hashCode = anObject.hashCode(); } /** * Passes through to actual key's hash code. */ public int hashCode() { return hashCode; } /** * Compares by reference. */ public boolean equals(Object anObject) { if (!(anObject instanceof ReferenceKey)) return false; Object key = get(); if (key == null) return false; return (key == ((ReferenceKey) anObject).get()); } } private static String debugString() { String result = ""; int count = 0; synchronized (instance()) { Object anObject; Iterator it = observableToObservers.keySet().iterator(); while (it.hasNext()) { result += ((Reference) it.next()).get() + " : "; count++; /* * Iterator values = ((List)it.next()).iterator(); while ( values.hasNext() ) { * anObject = ((Reference)values.next()).get(); if ( anObject != null ) { // if * ( anObject instanceof net.wotonomy.ui.MasterDetailAssociation ) result += * anObject.getClass().toString() + " : "; count++; } } */ } result += "[" + count + "]"; } return result; } } /* * $Log$ Revision 1.2 2006/02/16 16:47:14 cgruber Move some classes in to * "internal" packages and re-work imports, etc. * * Also use UnsupportedOperationExceptions where appropriate, instead of * WotonomyExceptions. * * Revision 1.1 2006/02/16 13:19:57 cgruber Check in all sources in * eclipse-friendly maven-enabled packages. * * Revision 1.9 2002/10/24 18:18:12 mpowers NSArray's are now considered * read-only, so we can return our internal representation to reduce unnecessary * object allocation. * * Revision 1.8 2001/02/21 21:17:32 mpowers Now retaining a reference to the * recent changes observer. Better documented need to retain reference. Started * implementing notifications. * * Revision 1.7 2001/02/17 16:52:05 mpowers Changes in imports to support * building with jdk1.1 collections. * * Revision 1.6 2001/02/05 18:45:45 mpowers Reduced access back to private for * utility methods. * * Revision 1.5 2001/02/05 18:42:32 mpowers Updated documentation throughout * project. * * Revision 1.4 2001/01/18 16:57:47 mpowers Added debug facility. * * Revision 1.3 2001/01/10 16:28:53 mpowers Implemented a compare-by-reference * weak hashtable because WeakHashMap compared by value and we can't assume that * display groups won't contain objects whose values are equivalent. * * Revision 1.2 2001/01/09 20:10:19 mpowers Now using weak references to track * observables and their observers. * * Revision 1.1.1.1 2000/12/21 15:46:44 mpowers Contributing wotonomy. * * Revision 1.8 2000/12/20 16:25:35 michael Added log to all files. * * */