/* * Bibliothek - DockingFrames * Library built on Java/Swing, allows the user to "drag and drop" * panels containing any Swing-Component the developer likes to add. * * Copyright (C) 2007 Benjamin Sigg * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Benjamin Sigg * benjamin_sigg@gmx.ch * CH - Switzerland */ package bibliothek.gui.dock.util; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import bibliothek.gui.DockController; import bibliothek.util.Path; /** * A map containing some string-values pairs and so called * bridges to modify these values when reading them out. * @author Benjamin Sigg * @param <V> The kind of values this map contains * @param <U> The kind of observers used to read values from this map * @param <B> The kind of bridges used to transfer values <code>V</code> to observers <code>U</code> */ public class UIProperties<V, U extends UIValue<V>, B extends UIBridge<V, U>> { /** the map of providers known to this manager */ private Map<Path, UIPriorityValue<B>> bridges = new HashMap<Path, UIPriorityValue<B>>(); /** how often some bridges are observed */ private Map<Path, Integer> bridgesAccess = new HashMap<Path, Integer>(); /** the map of resources that have been set */ private Map<String, UIPriorityValue<V>> resources = new HashMap<String, UIPriorityValue<V>>(); /** how often some resources are observed */ private Map<String, Integer> resourcesAccess = new HashMap<String, Integer>(); /** all the backup schemes for missing values (resources and bridges) */ private PriorityValue<UIScheme<V, U, B>> schemes = new PriorityValue<UIScheme<V,U,B>>(); /** all the listeners to the {@link #schemes} */ private PriorityValue<UISchemeListener<V, U, B>> schemeListeners = new PriorityValue<UISchemeListener<V,U,B>>(); /** a list of all observers */ private List<Observer> observers = new LinkedList<Observer>(); /** whether to stall updates or not */ private int updateLock = 0; /** the owner of this properties map */ private DockController controller; /** * Creates a new map. * @param controller the owner of this map */ public UIProperties( DockController controller ){ this.controller = controller; } /** * Gets the controller in whose realm this map is used. * @return the controller, not <code>null</code> */ public DockController getController(){ return controller; } /** * Tells this manager to stall all updates. No {@link UIValue} will * be informed when a color or provider changes. */ public void lockUpdate(){ updateLock++; } /** * Tells this manager no longer to stall updates. This triggers a full * update on all {@link UIValue}s. */ public void unlockUpdate(){ updateLock--; if( updateLock == 0 ){ for( Observer observer : observers ) observer.resetAll(); } } /** * Gets the {@link UIScheme} that is used to fill up missing values in * the level <code>priority</code>. * @param priority some priority * @return the scheme of that level or <code>null</code> * @see #setScheme(Priority, UIScheme) */ public UIScheme<V, U, B> getScheme( Priority priority ){ return schemes.get( priority ); } /** * Sets or removes an {@link UIScheme} for the level <code>priority</code> of this * {@link UIProperties}. The scheme will be used to fill missing values of this properties. Since * a "missing resource" cannot be removed, any attempt to delete a resource created by a scheme * must fail. * @param priority the level which will be provided with new values from <code>scheme</code>. * @param scheme the new scheme or <code>null</code> */ public void setScheme( final Priority priority, UIScheme<V, U, B> scheme ){ UIScheme<V, U, B> old = schemes.get( priority ); schemes.set( priority, scheme ); if( old != scheme ){ if( old != null ){ old.removeListener( schemeListeners.get( priority ) ); int count = 0; for( Priority p : Priority.values() ){ if( schemes.get( p ) == old ){ count++; } } if( count == 0 ){ old.uninstall( this ); } } if( scheme != null ){ int count = 0; for( Priority p : Priority.values() ){ if( schemes.get( p ) == scheme ){ count++; } } if( count == 1 ){ scheme.install( this ); } if( schemeListeners.get( priority ) == null ){ schemeListeners.set( priority, new UISchemeListener<V, U, B>(){ public void changed( UISchemeEvent<V, U, B> event ){ schemeUpdate( priority, event ); } }); } scheme.addListener( schemeListeners.get( priority ) ); } fullSchemeUpdate( priority ); } } private void fullSchemeUpdate( Priority priority ){ schemeUpdate( priority, new UISchemeEvent<V,U,B>(){ public Collection<Path> changedBridges( Set<Path> names ){ return null; } public Collection<String> changedResources( Set<String> names ){ return null; } public UIScheme<V,U,B> getScheme(){ return null; } }); } private void schemeUpdate( Priority priority, UISchemeEvent<V, U, B> event ){ try{ lockUpdate(); // collect changes Set<String> usedResources = getAllUsedResources(); Collection<String> changedResources = event.changedResources( usedResources ); if( changedResources == null ){ changedResources = usedResources; } Set<Path> usedBridges = getAllUsedBridges(); Collection<Path> changedBridges = event.changedBridges( usedBridges ); if( changedBridges == null ){ changedBridges = usedBridges; } UIScheme<V, U, B> scheme = schemes.get( priority ); // resources for( String name : changedResources ){ UIPriorityValue<V> value = resources.get( name ); V replacement = null; if( scheme != null ){ replacement = scheme.getResource( name, this ); } if( value == null ){ if( replacement != null ){ value = new UIPriorityValue<V>(); value.set( priority, replacement, scheme ); if( !isRemoveable( name, value )){ resources.put( name, value ); } } } else{ if( value.getScheme( priority ) == null ){ if( value.get( priority ) == null ){ value.set( priority, replacement, scheme ); } } else{ value.set( priority, replacement, scheme ); } if( isRemoveable( name, value )){ resources.remove( name ); } } } // bridges for( Path name : changedBridges ){ UIPriorityValue<B> value = bridges.get( name ); B replacement = null; if( scheme != null ){ replacement = scheme.getBridge( name, this ); } if( value == null ){ if( replacement != null ){ value = new UIPriorityValue<B>(); value.set( priority, replacement, scheme ); if( !isRemoveable( name, value )){ bridges.put( name, value ); } } } else{ if( value.getScheme( priority ) == null ){ if( value.get( priority ) == null ){ value.set( priority, replacement, scheme ); } } else{ value.set( priority, replacement, scheme ); } if( isRemoveable( name, value )){ bridges.remove( name ); } } } } finally{ unlockUpdate(); } } private Set<String> getAllUsedResources(){ Set<String> result = new HashSet<String>(); for( Observer observer : observers ){ result.add( observer.id ); } return result; } private Set<Path> getAllUsedBridges(){ Set<Path> result = new HashSet<Path>(); for( Observer observer : observers ){ result.add( observer.path ); } return result; } /** * Adds a new bridge between this {@link UIProperties} and a set of * {@link UIValue}s that have a certain type. * @param priority the importance of the new provider * @param path the path for which this bridge should be used. * @param bridge the new bridge */ public void publish( Priority priority, Path path, B bridge ){ if( priority == null ) throw new IllegalArgumentException( "priority must not be null" ); if( path == null ) throw new IllegalArgumentException( "path must not be null" ); if( bridge == null ) throw new IllegalArgumentException( "bridge must not be null" ); UIPriorityValue<B> value = bridges.get( path ); if( value == null ){ value = createBridge( path ); bridges.put( path, value ); } if( value.set( priority, bridge, null )){ if( updateLock == 0 ){ for( Observer check : observers ){ check.resetBridge(); } } } } /** * Removes the bridge that handles the {@link UIValue}s of kind <code>path</code>. Please note * that bridges created by the current {@link UIScheme} cannot be removed. Also note that the removed bridge * may be replaced by a bridge created by the current {@link UIScheme}. * @param priority the importance of the bridge * @param path the path of the bridge */ public void unpublish( Priority priority, Path path ){ UIPriorityValue<B> value = bridges.get( path ); if( value != null && value.getScheme( priority ) == null ){ UIScheme<V,U,B> scheme = schemes.get( priority ); B bridge = null; if( scheme != null ){ bridge = scheme.getBridge( path, this ); } boolean change = value.set( priority, bridge, scheme ); if( isRemoveable( path, value ) ){ bridges.remove( path ); } if( change && updateLock == 0 ){ for( Observer check : observers ){ check.resetBridge(); } } } } /** * Searches for all occurrences of <code>bridge</code> and removes them. Please note * that bridges created by the current {@link UIScheme} cannot be removed. Also note that the removed bridge * may be replaced by a bridge created by the current {@link UIScheme}. * All {@link UIValue}s that used <code>bridge</code> are redistributed. * @param priority the importance of the bridge * @param bridge the bridge to remove */ public void unpublish( Priority priority, B bridge ){ Iterator<Map.Entry<Path, UIPriorityValue<B>>> iterator = bridges.entrySet().iterator(); boolean change = false; UIScheme<V, U, B> scheme = schemes.get( priority ); while( iterator.hasNext() ){ Map.Entry<Path, UIPriorityValue<B>> entry = iterator.next(); UIPriorityValue<B> next = entry.getValue(); if( next.get( priority ) == bridge ){ if( next.getScheme( priority ) == null ){ B replacement = null; if( scheme != null ){ replacement = scheme.getBridge( entry.getKey(), this ); } change = next.set( priority, replacement, scheme ) || change; if( isRemoveable( entry.getKey(), next ) ){ iterator.remove(); } } } } if( change && updateLock == 0 ){ for( Observer check : observers ){ check.resetBridge(); } } } /** * Gets the bridge which is stored on level <code>priority</code> for {@link UIValue}s * of kind <code>path</code>. * @param priority the level in which to search * @param path the kind of the {@link UIValue}s * @return either <code>null</code>, a bridge that has been {@link #publish(Priority, Path, UIBridge) published} * or a bridge that was created by an {@link UIScheme} */ public B getBridge( Priority priority, Path path ){ UIPriorityValue<B> bridge = bridges.get( path ); if( bridge == null ){ bridge = createBridge( path ); if( !isRemoveable( path, bridge )){ bridges.put( path, bridge ); } } if( bridge != null ){ UIPriorityValue.Value<B> value = bridge.get( priority ); if( value != null ){ return value.getValue(); } } return null; } /** * Tells whether <code>bridge</code> is stored in this map. * @param bridge some object to search * @return <code>true</code> if <code>bridge</code> was found anywhere */ public boolean isStored( B bridge ){ for( UIPriorityValue<B> value : bridges.values() ){ for( Priority priority : Priority.values() ){ if( value.getValue( priority ) == bridge ){ return true; } } } return false; } /** * Tells whether the bridge with id <code>path</code> is observed by at least one {@link UIValue}. * @param path the name of some {@link UIBridge} * @return if <code>path</code> is observed */ public boolean isObserved( Path path ){ return bridgesAccess.containsKey( path ); } private boolean isRemoveable( Path path, UIPriorityValue<B> value ){ if( value.getValue() == null ){ return true; } if( value.isAllScheme() && !isObserved( path )){ return true; } return false; } private void checkRemove( Path path ){ UIPriorityValue<B> value = bridges.get( path ); if( value != null ){ if( isRemoveable( path, value )){ bridges.remove( path ); } } } /** * Installs a new {@link UIValue}. The value will be informed about * any change in the resource <code>id</code>. * @param id the id of the resource that <code>value</code> will monitor * @param path the kind of the value * @param value the new observer */ public void add( String id, Path path, U value ){ if( path == null ) throw new IllegalArgumentException( "path must not be null" ); if( id == null ) throw new IllegalArgumentException( "id must not be null" ); if( value == null ) throw new IllegalArgumentException( "value must not be null" ); Observer combination = new Observer( id, path, value ); observers.add( combination ); combination.resetAll(); } /** * Uninstalls an observer of a resource * @param value the observer to remove */ public void remove( U value ){ ListIterator<Observer> list = observers.listIterator(); while( list.hasNext() ){ Observer next = list.next(); if( next.getValue() == value ){ list.remove(); next.destroy(); return; } } } /** * Tells whether the value with id <code>id</code> is observed by at least one {@link UIValue}. * @param id the name of some value * @return if <code>id</code> is observed */ public boolean isObserved( String id ){ return resourcesAccess.containsKey( id ); } private boolean isRemoveable( String id, UIPriorityValue<V> value ){ if( value.getValue() == null ){ return true; } if( value.isAllScheme() && !isObserved( id )){ return true; } return false; } private void checkRemove( String id ){ UIPriorityValue<V> value = resources.get( id ); if( value != null ){ if( isRemoveable( id, value )){ resources.remove( id ); } } } /** * Searches a bridge that can be used for <code>path</code>. * @param path the kind of bridge that is searched. First a bridge for * <code>path</code> will be searched, then for the parent of <code>path</code>, * and so on... * @return the bridge or <code>null</code> */ protected B getBridgeFor( Path path ){ while( path != null ){ UIPriorityValue<B> bridge = bridges.get( path ); if( bridge == null ){ bridge = createBridge( path ); if( !isRemoveable( path, bridge )){ bridges.put( path, bridge ); } } if( bridge != null ){ B result = bridge.getValue(); if( result != null ) return result; } path = path.getParent(); } return null; } /** * Sets a new resource and informs all {@link UIValue} that are observing <code>id</code> about the change. * Please note that values created by an {@link UIScheme} cannot be removed, and that a removed value may * be replaced by a value of an {@link UIScheme}. * @param priority the importance of this value * @param id the name of the value * @param resource the new resource, can be <code>null</code> */ public void put( Priority priority, String id, V resource ){ UIPriorityValue<V> value = resources.get( id ); if( value == null && resource != null ){ value = createResource( id ); resources.put( id, value ); } if( value != null ){ UIScheme<V, U, B> scheme = null; if( resource == null ){ scheme = schemes.get( priority ); if( scheme != null ){ resource = scheme.getResource( id, this ); } } if( value.set( priority, resource, scheme ) ){ if( updateLock == 0 ){ for( Observer observer : observers ){ if( observer.id.equals( id )){ observer.update( resource ); } } } } if( isRemoveable( id, value ) ){ resources.remove( id ); } } } /** * Gets a resource. * @param id the id of the resource * @return the resource or <code>null</code> * @see #put(Priority, String, Object) */ public V get( String id ){ UIPriorityValue<V> value = resources.get( id ); if( value == null ){ value = createResource( id ); if( !isRemoveable( id, value )){ resources.put( id, value ); } } return value == null ? null : value.getValue(); } /** * Call {@link UIValue#set(Object)} with the matching value that is stored in this * map for <code>id</code>. * @param id the unique identifier of the value to read * @param kind the kind of value <code>key</code> is * @param key the destination of the value */ public void get( String id, Path kind, U key ){ V base = get( id ); B bridge = getBridgeFor( kind ); if( bridge != null ){ bridge.set( id, base, key ); } else{ key.set( base ); } } /** * Removes all values that stored under the given priority. Values created by an {@link UIScheme} are * not affected by this call. * @param priority the priority whose elements should be removed */ public void clear( Priority priority ){ UIScheme<V, U, B> scheme = schemes.get( priority ); Iterator<Map.Entry<String, UIPriorityValue<V>>> resources = this.resources.entrySet().iterator(); while( resources.hasNext() ){ Map.Entry<String, UIPriorityValue<V>> entry = resources.next(); UIPriorityValue<V> value = entry.getValue(); if( value.getScheme( priority ) == null ){ V replacement = null; if( scheme != null ){ replacement = scheme.getResource( entry.getKey(), this ); } value.set( priority, replacement, scheme ); if( isRemoveable( entry.getKey(), value )){ resources.remove(); } } } Iterator<Map.Entry<Path, UIPriorityValue<B>>> bridges = this.bridges.entrySet().iterator(); while( bridges.hasNext() ){ Map.Entry<Path, UIPriorityValue<B>> entry = bridges.next(); UIPriorityValue<B> value = entry.getValue(); if( value.getScheme( priority ) == null ){ B replacement = null; if( scheme != null ){ replacement = scheme.getBridge( entry.getKey(), this ); } value.set( priority, replacement, scheme ); if( isRemoveable( entry.getKey(), value )){ bridges.remove(); } } } if( updateLock == 0 ){ for( Observer observer : observers ){ observer.resetAll(); } } } /** * Sets up a new {@link PriorityValue} for the bridge with name <code>path</code>, the * {@link PriorityValue} is pre-filled with the values of all the schemes known to this properties. * @param path the path of some bridge * @return the new filled value */ private UIPriorityValue<B> createBridge( Path path ){ UIPriorityValue<B> result = new UIPriorityValue<B>(); for( Priority priority : Priority.values() ){ UIScheme<V, U, B> scheme = schemes.get( priority ); if( scheme != null ){ B value = scheme.getBridge( path, this ); if( value != null ){ result.set( priority, value, scheme ); } } } return result; } /** * Creates a new {@link PriorityValue} that is set up with the resources of the current {@link UIScheme}s. * @param name the name of the value * @return the new value */ private UIPriorityValue<V> createResource( String name ){ UIPriorityValue<V> result = new UIPriorityValue<V>(); for( Priority priority : Priority.values() ){ UIScheme<V, U, B> scheme = schemes.get( priority ); if( scheme != null ){ V value = scheme.getResource( name, this ); if( value != null ){ result.set( priority, value, scheme ); } } } return result; } /** * Represents the combination of a resource, an {@link UIValue} and its * {@link UIBridge}. * @author Benjamin Sigg */ private class Observer{ /** the id of the observed resource */ private String id; /** the kind of value this observers bridge handles */ private Path path; /** the observer which gets informed about changed resources */ private U value; /** a bridge for modified resources */ private B bridge; /** * Creates a new observer * @param id the id of the observed resource * @param path the type of observer that is wrapped by this <code>Observer</code> * @param value the listener for changed resources */ public Observer( String id, Path path, U value ){ this.id = id; this.path = path; this.value = value; Integer count = bridgesAccess.get( path ); if( count == null ){ count = 1; } else{ count = count+1; } bridgesAccess.put( path, count ); count = resourcesAccess.get( id ); if( count == null ){ count = 1; } else{ count = count+1; } resourcesAccess.put( id, count ); } /** * Tells this observer to release resources. */ public void destroy(){ setBridge( null, false ); Integer count = bridgesAccess.get( path ); if( count == 1 ){ bridgesAccess.remove( path ); checkRemove( path ); } else{ bridgesAccess.put( path, count-1 ); } count = resourcesAccess.get( id ); if( count == 1 ){ resourcesAccess.remove( id ); checkRemove( id ); } else{ resourcesAccess.put( id, count-1 ); } } /** * Gets the listener for changed resources. * @return the listener */ public U getValue() { return value; } /** * Updates resource and bridge of this <code>Observer</code>. */ public void resetAll(){ B bridge = getBridgeFor( path ); if( bridge == null ) update( get( id )); else setBridge( bridge, true ); } /** * Ensures that the correct {@link UIBridge} is used. */ public void resetBridge(){ setBridge( getBridgeFor( path ), false ); } /** * Sets the {@link UIBridge} of this <code>Observer</code>. * @param bridge the new bridge, can be <code>null</code> * @param force if <code>true</code>, than an update of the resources will * be done anyway. Otherwise an update will only be done if a new * bridge is set. */ public void setBridge( B bridge, boolean force ) { if( this.bridge != bridge ){ if( this.bridge != null ) this.bridge.remove( id, value ); this.bridge = bridge; if( this.bridge != null ){ this.bridge.add( id, value ); } update( get( id )); } else if( force ){ update( get( id )); } } /** * Called when a resource has been exchanged and the callback of this * <code>Observer</code> has to be informed. * @param value the new value of the resource, can be <code>null</code> */ public void update( V value ){ if( bridge == null ) this.value.set( value ); else bridge.set( id, value, this.value ); } } }