/* * BioJava development code * * This code may be freely distributed and modified under the * terms of the GNU Lesser General Public Licence. This should * be distributed with the code. If you do not have a copy, * see: * * http://www.gnu.org/copyleft/lesser.html * * Copyright for this code is held jointly by the individual * authors. These should be listed in @author doc comments. * * For more information on the BioJava project and its aims, * or to join the biojava-l mailing list, visit the home page * at: * * http://www.biojava.org/ */ package org.biojava.utils; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** *

* A utility class to provide management for informing ChangeListeners of * ChangeEvents. *

* *

* This is loosely modelled after the standard PropertyChangeEvent objects. *

* *

* For an object to correctly fire these events, they must follow a broad * outline like this: *

 * public void mutator(foo arg) throw ChangeVetoException {
 *   ChangeEvent cevt = new ChangeEvent(this, SOME_EVENT_TYPE, arg);
 *   synchronized(changeSupport) {
 *     changeSupport.firePreChangeEvent(cevt);
 *     // update our state using arg
 *     // ...
 *     changeSupport.firePostChangeEvent(cevt);
 *   }
 * }
 * 
*

* *

* The methods that delegate adding and removing listeners to a ChangeSupport * must take responsibility for synchronizing on the delegate. *

* * @author Matthew Pocock * @author Thomas Down * @author Keith James (docs) * @author Kalle Naslund (tiny bugfix) * @since 1.1 */ public class ChangeSupport { private int listenerCount; private int delta; private Set unchanging; private Reference[] listeners; private ChangeType[] types; /** * Return true if we have any listeners registered at all. * * @return true if there are listeners */ public boolean hasListeners() { return (listenerCount > 0); } /** * Return true if we have listeners registered for a particular change type. * * @param ct the ChangeType to check * @return true if there are listeners for this type */ public boolean hasListeners(ChangeType ct) { for(int i = 0; i < listenerCount ; i++ ) { ChangeType type = (ChangeType) types[i]; if(ct.isMatchingType(type)) { return true; } } return false; } /** * Generate a new ChangeSupport instance. */ public ChangeSupport() { this(5); } /** * Generate a new ChangeSupport instance which has room for initialSize * listeners before it needs to grow any resources. * * @param initialSize the number of listeners that can be added before this * needs to grow for the first time */ public ChangeSupport(int initialSize) { this(initialSize, 5); } /** * Generate a new ChangeSupport instance which has room for initialSize * listeners before it needs to grow any resources, and which will grow by * delta each time. * * @param initialSize the number of listeners that can be added before this * needs to grow for the first time * @param delta the number of listener slots that this will grow by each time * it needs to */ public ChangeSupport(int initialSize, int delta) { this(Collections.EMPTY_SET, initialSize, delta); } public ChangeSupport(Set unchanging) { this(unchanging, 0, 5); } /** * Generate a new ChangeSupport instance which has room for initialSize * listeners before it needs to grow any resources, and which will grow by * delta each time. * * @param unchanging Set of ChangeTypes that can never be fired * @param initialSize the number of listeners that can be added before this * needs to grow for the first time * @param delta the number of listener slots that this will grow by each time * it needs to */ public ChangeSupport(Set unchanging, int initialSize, int delta) { this.listenerCount = 0; this.listeners = new Reference[initialSize]; this.types = new ChangeType[initialSize]; this.delta = delta; this.unchanging = new HashSet(unchanging); } /** * Add a listener that will be informed of all changes. * * @param cl the ChangeListener to add */ public void addChangeListener(ChangeListener cl) { addChangeListener(cl, ChangeType.UNKNOWN); } /** * Add a listener that will be informed of changes of a given type (and it's subtypes) * * @param cl the ChangeListener * @param ct the ChangeType it is to be informed of */ public void addChangeListener(ChangeListener cl, ChangeType ct) { if (ct == null) { throw new NullPointerException("Since 1.2, listeners registered for the null changetype are not meaningful. Please register a listener for ChangeType.UNKNOWN instead"); } if(isUnchanging(ct)) { return; } synchronized(this) { growIfNecessary(); types[listenerCount] = ct; listeners[listenerCount] = new WeakReference(cl); listenerCount++; } } /** * Grows the internal resources if by adding one more listener they would be * full. */ protected void growIfNecessary() { //try cleaning up first synchronized(this){ reapGarbageListeners(); } if(listenerCount == listeners.length) { int newLength = listenerCount + delta; Reference[] newList = new Reference[newLength]; ChangeType[] newTypes = new ChangeType[newLength]; System.arraycopy(listeners, 0, newList, 0, listenerCount); System.arraycopy(types, 0, newTypes, 0, listenerCount); listeners = newList; types = newTypes; } } /** * Remove a listener that was interested in all types of changes. * * @param cl a ChangeListener to remove */ public void removeChangeListener(ChangeListener cl) { removeChangeListener(cl, ChangeType.UNKNOWN); } /** * Remove a listener that was interested in a specific types of changes. * * @param cl a ChangeListener to remove * @param ct the ChangeType that it was interested in */ public void removeChangeListener(ChangeListener cl, ChangeType ct) { synchronized(this) { for(int i = 0; i < listenerCount; i++) { if( (listeners[i].get() == cl) && (types[i] == ct) ) { listenerCount--; System.arraycopy(listeners, i+1, listeners, i, (listenerCount - i)); System.arraycopy(types, i+1, types, i, (listenerCount - i)); return; } } } } /** * Remove all references to listeners which have been cleared by the * garbage collector. This method should only be called when the * object is locked. */ protected void reapGarbageListeners() { int pp = 0; for (int p = 0; p < listenerCount; ++p) { Reference r = listeners[p]; if (r.get() != null) { types[pp] = types[p]; listeners[pp] = r; pp++; }else{ //if it is null release the reference r = null; } } listenerCount = pp; } /** *

* Inform the listeners that a change is about to take place using their * firePreChangeEvent methods. *

* *

* Listeners will be informed if they were interested in all types of event, * or if ce.getType() is equal to the type they are registered for. *

* *

* This method must be called while the current thread holds the lock on this change support. *

* * @param ce the ChangeEvent to pass on * @throws ChangeVetoException if any of the listeners veto this change */ public void firePreChangeEvent(ChangeEvent ce) throws ChangeVetoException { assert Thread.holdsLock(this) : "firePreChangeEvent must be called in a synchronized block locking the ChangeSupport"; boolean needToReap = false; ChangeType ct = ce.getType(); int listenerCount = this.listenerCount; ChangeType[] types = new ChangeType[listenerCount]; System.arraycopy(this.types, 0, types, 0, listenerCount); Reference[] listeners = new Reference[listenerCount]; System.arraycopy(this.listeners, 0, listeners, 0, listenerCount); for(int i = 0; i < listenerCount; i++) { ChangeType lt = types[i]; if( ct.isMatchingType(lt)) { ChangeListener cl = (ChangeListener) listeners[i].get(); if (cl != null) { cl.preChange(ce); } else { needToReap = true; } } } if (needToReap) { reapGarbageListeners(); } } /** *

* Inform the listeners that a change has taken place using their * firePostChangeEvent methods. *

* *

* Listeners will be informed if they were interested in all types of event, * or if ce.getType() is equal to the type they are registered for. *

* *

* This method must be called while the current thread holds the lock on this change support. *

* * @param ce the ChangeEvent to pass on */ public void firePostChangeEvent(ChangeEvent ce) { assert Thread.holdsLock(this) : "firePostChangeEvent must be called in a synchronized block locking the ChangeSupport"; boolean needToReap = false; ChangeType ct = ce.getType(); int listenerCount = this.listenerCount; ChangeType[] types = new ChangeType[listenerCount]; System.arraycopy(this.types, 0, types, 0, listenerCount); Reference[] listeners = new Reference[listenerCount]; System.arraycopy(this.listeners, 0, listeners, 0, listenerCount); for(int i = 0; i < listenerCount; i++) { ChangeType lt = types[i]; if( ct.isMatchingType(lt) ) { ChangeListener cl = (ChangeListener) listeners[i].get(); if (cl != null) { cl.postChange(ce); } else { needToReap = true; } } } if (needToReap) { reapGarbageListeners(); } } public boolean isUnchanging(ChangeType ct) { if(unchanging == null) { return false; } for(Iterator i = ct.matchingTypes(); i.hasNext(); ) { if(unchanging.contains(i.next())) { return true; } } return false; } public String displayString() { StringBuffer sb = new StringBuffer(); sb.append(this.toString()); sb.append("\n"); for(int i = 0; i < listenerCount; i++) { sb.append("\t"); sb.append(listeners[i].get()); sb.append("\t"); sb.append(types[i]); sb.append("\n"); } return sb.toString(); } }