/** * */ package org.autoplot.dom; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; /** * Support class for encapsulating implementation of pendingChanges and mutator locks. * * PendingChanges are a way of notifying the bean and other clients using the bean that changes are coming to * the bean. Clients should use registerPendingChange to indicate that changes are coming. * performingChange indicates the change is in progress. changePerformed indicates to other clients * and the bean that the change is complete. For example, event thread registers pending change * and creates runnable object. A new thread is started. On the new thread, performingChange * and changePerformed is called. * * mutatorLock() is a way for a client to get exclusive, read-only access to a bean. * This also sets the valueAdjusting property. WARNING: this is improperly implemented, * and clients must check valueIsAdjusting to see if another lock is out. * * See http://das2.org/wiki/index.php/Pending_changes * @author jbf */ public final class ChangesSupport { Map changesPending; // number of said changes, typically 1. Map changeCount; // threads, for debugging. Map threads; WeakReference parent; private static final Logger logger= org.das2.util.LoggerManager.getLogger( "autoplot.dom.change" ); /** * if the propertyChangeSupport is provided, then change messages will be sent to * it directly. If null, then one is created with the parent as the source. * @param pcs * @param parent the object this is supporting, for debugging purposes. */ ChangesSupport( PropertyChangeSupport pcs, Object parent ) { this.parent= new WeakReference<>(parent); this.changesPending= new HashMap<>(); // lockObject -> client this.changeCount= new HashMap<>(); this.threads= new HashMap<>(); // lockObject -> thread ID. if ( pcs==null ) { pcs= new PropertyChangeSupport(parent); } this.propertyChangeSupport= pcs; } /** * returns the clients who have registered the change. Note this * implementation only allows for one client for each lock object. * @param lockObject object identifying the change. * @return clients who have registered the change. */ synchronized List whoIsChanging( Object lockObject ) { String msg= "whoIsChanging "+lockObject; logger.fine( msg ); Object client= changesPending.get(lockObject); if ( client==null ) { return Collections.emptyList(); } else { return Collections.singletonList(client); } } /** * the client knows a change will be coming, and the canvas' clients should * know that its current state will change soon. Example pending changes * would be: * layout because tick labels are changing * data is loading * * @param client the object that will perform the change. This allows the * canvas (and developers) identify who has registered the change. * @param lockObject object identifying the change. */ synchronized void registerPendingChange( Object client, Object lockObject ) { String msg= "registerPendingChange "+lockObject+" by "+client + " in "+ parent.get() ; logger.fine( msg ); Object existingClient= changesPending.get(lockObject); if ( existingClient!=null ) { if ( existingClient!=client ) { throw new IllegalStateException( "lock object in use: "+lockObject + ", by "+changesPending.get(lockObject) ); } else if ( existingClient==client ) { logger.log( Level.FINE, "bug 1075: second change registered but the first was not done."); // this is somewhat harmless. Don't bug clients with this message. } else { return; } } boolean oldVal= this.isPendingChanges(); changesPending.put( lockObject, client ); threads.put( lockObject, Thread.currentThread().toString() ); propertyChangeSupport.firePropertyChange( PROP_PENDINGCHANGES, oldVal, isPendingChanges() ); } /** * performingChange tells that the change is about to be performed. This * is a place holder in case we use a mutator lock, but currently does * nothing. If the change has not been registered, it will be registered implicitly. * This will increment the internal count of how many times the change * ought to occur. * @param client the object that is mutating the bean. * @param lockObject an object identifying the change. */ synchronized void performingChange( Object client, Object lockObject ) { Object ownerClient= changesPending.get(lockObject); if ( ownerClient==null || ownerClient!=client ) { if ( ownerClient!=null && ownerClient!=client ) { logger.log(Level.INFO, "performingChange by client object is not owner {0}", client ); } registerPendingChange( client, lockObject ); } Integer count= changeCount.get(lockObject); if ( count==null ) { changeCount.put( lockObject, 1 ); } else { changeCount.put( lockObject, count+1 ); } logger.log( Level.FINE, "performingChange {0} by {1} in {2}", new Object[]{lockObject, client, parent}); } /** * the change is complete, and as far as the client is concerned, the canvas * is valid. This will decrement the count of how many times the change ought to occur. * @param lockObject object identifying the change. */ synchronized void changePerformed( Object client, Object lockObject ) { logger.log( Level.FINE, "clearPendingChange {0} by {1} in {2}", new Object[]{lockObject, client, parent}); Integer count= changeCount.get(lockObject); Object ownerClient= changesPending.get(lockObject); if ( ownerClient==null ) { // throw new IllegalStateException( "no such lock object: "+lockObject ); //TODO: handle multiple registrations by the same client logger.log(Level.INFO, "no lock object found for {0}", lockObject); } else if ( ownerClient!=client ) { logger.log(Level.INFO, "change performed client object is not owner {0}", ownerClient ); } boolean oldVal= this.isPendingChanges(); if ( count==null ) { logger.log(Level.INFO, "expect value for changeCount {0}, was performingChange called?", lockObject); count= 0; } else { count= count-1; } if ( count==0 ) { changesPending.remove(lockObject); threads.remove( lockObject ); changeCount.remove(lockObject); } else if ( count>0) { changeCount.put(lockObject,count); } else { throw new IllegalStateException("what happened here--changeCount<0!"); } propertyChangeSupport.firePropertyChange( PROP_PENDINGCHANGES, oldVal, isPendingChanges() ); } public static final String PROP_PENDINGCHANGES = "pendingChanges"; /** * someone has registered a pending change. * See getChangesPending. * @return true if someone has registered a pending change. */ public boolean isPendingChanges() { return changesPending.size() > 0; } /** * allow check for particular change. * @param lockObject object identifying the change. * @return true if that particular change is pending. */ public boolean isPendingChanges( Object lockObject ) { return changesPending.containsKey(lockObject); } /** * return a map listing the pending changes. This is a thread-safe * read-only copy. * @return a map listing the pending changes. */ public synchronized Map getChangesPending() { if ( changesPending.isEmpty() ) { return Collections.emptyMap(); } else { return new HashMap(changesPending); } } /** * null, "", or a description of the change */ public static final String PROP_VALUEADJUSTING = "valueAdjusting"; /** * Check if the bean state is rapidly changing. This * returns the lock message, or null if the value * is not adjusting. * @return null or a message indicating that the value is adjusting. */ public String isValueAdjusting() { return valueIsAdjusting; } private String valueIsAdjusting = null; private final DomLock mutatorLock = new DomLock(); public class DomLock extends ReentrantLock { public DomLock( ) { } public void lock( String description ) { super.lock(); if (valueIsAdjusting!=null) { //System.err.println("lock is already set!"); //nested locks result in the outermost lock being used while inner locks are ignored. } else { valueIsAdjusting= description; propertyChangeSupport.firePropertyChange( PROP_VALUEADJUSTING, null, description ); } } @Override public void lock() { this.lock(""); } @Override public void unlock() { super.unlock(); if ( !super.isLocked() ) { String old= valueIsAdjusting; valueIsAdjusting = null; propertyChangeSupport.firePropertyChange( PROP_VALUEADJUSTING, old, valueIsAdjusting ); } else { //System.err.println("lock is still set, neat!"); } } } /** * one client will have write access to the bean, and when unlock * is called, a "valueAdjusting" property change event is fired. * In the future, this * will return null if the lock is already out, but for now, * clients should check the valueIsAdjusting property. * @return */ protected synchronized DomLock mutatorLock() { return mutatorLock; } private final PropertyChangeSupport propertyChangeSupport; public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } @Override public String toString() { return "changeSupport: "+ changesPending; } }