package org.das2.system; 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.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import org.das2.util.LoggerManager; /** * 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. * mutatorLock() is a way for a client to get exclusive, read-only access to a bean. This also sets the valueAdjusting * property. * * See http://das2.org/wiki/index.php/Pending_changes (Wiki was lost, but may be recoverable.) * @author jbf */ public class ChangesSupport { Map changesPending; // Number of said changes, typically 1. Map changeCount; // threads, for debugging. Map threads; WeakReference parent; private static final Logger logger = LoggerManager.getLogger("das2.system.changes"); /** * 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 the PropertyChangeSupport * @param parent the object this is supporting, for debugging purposes. */ public ChangesSupport(PropertyChangeSupport pcs, Object parent) { this.parent = new WeakReference<>(parent); this.changesPending = new HashMap<>(); // client->lock 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 *
* Note, it is okay to call this multiple times for the same client and lock object. * @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. */ public synchronized void registerPendingChange(Object client, Object lockObject) { logger.log(Level.FINE, "registerPendingChange {0} by {1} in {2}", new Object[]{lockObject, client, parent.get()}); 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. } } Integer count= changeCount.get(lockObject); if ( count==null ) { changeCount.put( lockObject, 1 ); } else { changeCount.put( lockObject, count+1 ); } boolean oldVal = this.isPendingChanges(); changesPending.put(lockObject, client); threads.put( lockObject, Thread.currentThread().toString() ); propertyChangeSupport.firePropertyChange(PROP_PENDINGCHANGES, oldVal, true); } /** * return a list of all the pending changes. These are returned in a * Map that goes from pending change to change manager. * * @param changes a Map to which the changes will be added. * @see #getChangesPending */ public synchronized void pendingChanges( Map changes ) { changes.putAll( changesPending ); } /** * 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. */ public 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 ); } 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. * @param client the object that is mutating the bean. * @param lockObject an object identifying the change. */ public 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, true); } public static final String PROP_PENDINGCHANGES = "pendingChanges"; /** * true if someone has registered a pending change. * @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. * @see #pendingChanges */ public synchronized Map getChangesPending() { if ( changesPending.isEmpty() ) { return Collections.emptyMap(); } else { return new HashMap(changesPending); } } public static final String PROP_VALUEADJUSTING = "valueAdjusting"; /** * true when the bean state is rapidly changing. * @return true when the bean state is rapidly changing. */ public boolean isValueAdjusting() { return valueIsAdjusting; } private boolean valueIsAdjusting = false; private final Lock mutatorLock = new ReentrantLock() { @Override public void lock() { super.lock(); if (valueIsAdjusting) { //System.err.println("lock is already set!"); } else { propertyChangeSupport.firePropertyChange( PROP_VALUEADJUSTING, false, true ); valueIsAdjusting = true; } } @Override public void unlock() { super.unlock(); if ( !super.isLocked() ) { valueIsAdjusting = false; propertyChangeSupport.firePropertyChange( PROP_VALUEADJUSTING, true, false ); } 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 the lock or null. */ public synchronized Lock mutatorLock() { return mutatorLock; } private PropertyChangeSupport propertyChangeSupport; public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } }