/* * 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) 2011 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; import java.awt.EventQueue; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; import bibliothek.gui.DockController; import bibliothek.gui.DockStation; import bibliothek.gui.Dockable; /** * The {@link DockHierarchyLock} allows {@link DockStation}s to defend * themselves against concurrent modifications of the hierarchy. At any time only * one {@link DockStation} or a class working with {@link DockStation}s in the realm * of a {@link DockController} can acquire the lock. * @author Benjamin Sigg */ public class DockHierarchyLock { /** the current lock */ private volatile Token token = null; /** whether to throw an exception or just print one */ private boolean hardExceptions = false; /** if greater than 0, no exception is ever thrown */ private volatile int concurrent = 0; /** the {@link Runnable}s to execute once a {@link Token} is released */ private Queue<Runnable> onRelease = new LinkedList<Runnable>(); /** whether a {@link Runnable} from {@link #onRelease} is currently executed */ private boolean onReleaseRunning = false; /** * Sets whether exceptions should be thrown or only printed. * @param hardExceptions <code>true</code> if the exceptions should be thrown */ public void setHardExceptions( boolean hardExceptions ){ this.hardExceptions = hardExceptions; } /** * Tells whether hard exceptions should be thrown or only printed. * @return <code>true</code> if exceptions should be thrown * @see #setHardExceptions(boolean) */ public boolean isHardExceptions(){ return hardExceptions; } /** * Tells this lock whether concurrent modifications are allowed or not. If allowed no exception will be thrown * due to a lock being acquired while a lock is already held. * @param concurrent whether to allow concurrent modification or not */ public synchronized void setConcurrent( boolean concurrent ){ if( concurrent ){ this.concurrent++; } else{ this.concurrent--; } } /** * Whether this lock throws exceptions or is silent. * @return if <code>true</code>, then this lock is silent * @see #isConcurrent() */ public boolean isConcurrent(){ return concurrent > 0; } /** * Executes <code>run</code> once no {@link Token} is acquired anymore. The exact order of how and when * the {@link Runnable}s are executed is: * <ul> * <li>Any token acquired while {@link #isConcurrent()} returns <code>true</code> will be ignored.</li> * <li>There is a queue of {@link Runnable}s, <code>run</code> will be added to the end of that queue. The queue * will be executed from the beginning to the end, this order cannot be changed.</li> * <li>If this is a recursive call, then <code>run</code> will never be executed directly.</li> * <li>If currently no {@link Token} is acquired, then <code>run</code> is executed immediately.</li> * <li><code>run</code> is always executed in the EDT, other {@link Thread}s may be blocked until <code>run</code> * is completed</li> * </ul> * @param run the {@link Runnable} to executed, not <code>null</code> * @throws IllegalArgumentException if <code>run</code> is <code>null</code> * @throws IllegalStateException if an {@link InterruptedException} is caught */ public void onRelease( Runnable run ){ if( run == null ){ throw new IllegalArgumentException( "run must not be null" ); } synchronized( onRelease ) { onRelease.add( run ); } runOnRelease(); } private void runOnRelease(){ if( EventQueue.isDispatchThread() ){ if( !onReleaseRunning ){ synchronized( this ) { if( token != null ){ return; } } try{ onReleaseRunning = true; while( true ){ Runnable run = null; synchronized( onRelease ){ run = onRelease.poll(); } if( run == null ){ break; } run.run(); } } finally{ onReleaseRunning = false; } } } else{ final AtomicBoolean blocking = new AtomicBoolean( true ); EventQueue.invokeLater( new Runnable(){ public void run(){ runOnRelease(); synchronized(blocking){ blocking.set( false ); blocking.notifyAll(); } } }); long timeout = System.currentTimeMillis() + 1000; synchronized( blocking ){ while( blocking.get() ){ long now = System.currentTimeMillis(); if( timeout <= now ){ break; } try { blocking.wait( timeout - now ); } catch( InterruptedException e ) { // ignore } } } } } /** * The same as calling {@link #acquireLink(DockStation, Dockable)} with the {@link DockHierarchyLock} of * the {@link DockController} of <code>station</code>. Returns a fake {@link Token} if <code>station</code> has * no {@link DockController}. * @param station the station which wants to be the new parent of <code>dockable</code> * @param dockable a dockable with no parent * @return the acquired token to release the lock * @throws IllegalStateException if <code>dockable</code> has a parent or <code>station</code> * thinks that <code>dockable</code> is one of its children */ public static Token acquireLinking( DockStation station, Dockable dockable ){ DockController controller = station.getController(); if( controller == null ){ return new Token( null, station, dockable, true ); } else{ return controller.getHierarchyLock().acquireLink( station, dockable ); } } /** * The same as calling {@link #acquireUnlink(DockStation, Dockable)} with the {@link DockHierarchyLock} of * the {@link DockController} of <code>station</code>. Returns a fake {@link Token} if <code>station</code> has * no {@link DockController}. * @param station the current parent of <code>dockable</code> * @param dockable a dockable with <code>station</code> as parent * @return the acquired token to release the lock * @throws IllegalStateException if <code>dockable</code> is not a child of * <code>station</code> */ public static Token acquireUnlinking( DockStation station, Dockable dockable ){ DockController controller = station.getController(); if( controller == null ){ return new Token( null, station, dockable, true ); } else{ return controller.getHierarchyLock().acquireUnlink( station, dockable ); } } /** * The same as calling {@link #acquire(DockStation)} with the {@link DockHierarchyLock} of * the {@link DockController} of <code>station</code>. Returns a fake {@link Token} if <code>station</code> has * no {@link DockController}. * @param station the station to lock * @return the acquired token to release the lock * @throws IllegalStateException if a lock has already been acquired */ public static Token acquiring( DockStation station ){ DockController controller = station.getController(); if( controller == null ){ return new Token( null, station ); } else{ return controller.getHierarchyLock().acquire( station ); } } /** * Acquires a fake token which does not lock anything. This method never throws an exception. * @return the fake token */ public static Token acquireFake(){ return new Token( null, null, null, false ); } private String defaultMessage(){ return "During an operation the framework attempted to acquire the same lock twice. There are two possible explanations:\n" + "1. In a multi-threaded application one or both operations are not executed in the EventDispatchThread, or\n" + "2. The operations are calling each other, which should not happen.\n" + "Please verify that this application is not accessing the framework from different threads, and fill a bug" + "report if you feel that this exception is not caused by your application."; } /** * Allows <code>station</code> to become the new parent of <code>dockable</code>. * @param station the station which wants to be the new parent of <code>dockable</code> * @param dockable a dockable with no parent * @return the acquired token to release the lock * @throws IllegalStateException if <code>dockable</code> has a parent or <code>station</code> * thinks that <code>dockable</code> is one of its children */ public synchronized Token acquireLink( DockStation station, Dockable dockable ){ if( station == null ){ throw new IllegalArgumentException( "station is null" ); } if( dockable == null ){ throw new IllegalArgumentException( "dockable is null" ); } ensureUnlinked( station, dockable ); if( isConcurrent() ){ return new Token( this, station, dockable, true ); } if( token != null ){ throwException( new IllegalStateException( defaultMessage() ) ); } token = new Token( this, station, dockable, true ); return token; } /** * Allows <code>station</code> to remove itself as parent from <code>dockable</code>. * @param station the current parent of <code>dockable</code> * @param dockable a dockable with <code>station</code> as parent * @return the acquired token to release the lock * @throws IllegalStateException if <code>dockable</code> is not a child of * <code>station</code> */ public synchronized Token acquireUnlink( DockStation station, Dockable dockable ){ if( station == null ){ throw new IllegalArgumentException( "station is null" ); } if( dockable == null ){ throw new IllegalArgumentException( "dockable is null" ); } ensureLinked( station, dockable ); if( isConcurrent() ){ return new Token( this, station, dockable, false ); } if( token != null ){ throwException( new IllegalStateException( defaultMessage() ) ); } token = new Token( this, station, dockable, false ); return token; } /** * Acquires a lock describing the entire contents of <code>station</code>. This * method is intended in situations where the layout of <code>station</code> gets * modified and this modification must not be interrupted. * @param station the station for which a lock is requested * @return the acquired token to release the lock * @throws IllegalStateException if the lock is already acquired */ public synchronized Token acquire( DockStation station ){ if( isConcurrent() ){ return new Token( this, station ); } if( token != null ){ throwException( new IllegalStateException( defaultMessage() ) ); } token = new Token( this, station ); return token; } private void ensureLinked( DockStation station, Dockable dockable ){ if( dockable.getDockParent() != station ){ throwException( new IllegalStateException( "the parent of '" + dockable + "' is not '" + station + "' but '" + dockable.getDockParent() + "'" ) ); return; } boolean found = false; for( int i = 0, n = station.getDockableCount(); i<n && !found; i++ ){ if( station.getDockable( i ) == dockable ){ found = true; } } if( !found ){ throwException( new IllegalStateException( "the station '" + station + "' does not know '" + dockable + "'" ) ); return; } } private void ensureUnlinked( DockStation station, Dockable dockable ){ if( dockable.getDockParent() != null ){ throwException( new IllegalStateException( "The parent of '" + dockable + "' is not null but '" + dockable.getDockParent() + "'" ) ); return; } for( int i = 0, n = station.getDockableCount(); i<n; i++ ){ if( station.getDockable( i ) == dockable ){ throwException( new IllegalStateException( "The station '" + station + "' knows of '" + dockable + "'" ) ); return; } } } private void throwException( RuntimeException exception ){ if( hardExceptions ){ throw exception; } else{ exception.printStackTrace(); } } /** * Is acquired from a {@link DockHierarchyLock} and releases the lock. */ public static class Token{ private DockHierarchyLock lock; private DockStation station; private Dockable dockable; private boolean link; private Token( DockHierarchyLock lock, DockStation station ){ this.lock = lock; this.station = station; } private Token( DockHierarchyLock lock, DockStation station, Dockable dockable, boolean link ){ this.lock = lock; this.station = station; this.dockable = dockable; this.link = link; } /** * Releases the lock. * @throws IllegalStateException if the state is not as suggested by the * acquirer when acquiring the lock * @throws IllegalStateException if blocking access to the EDT was required and an {@link InterruptedException} * occurred */ public void release(){ synchronized( this ){ if( lock != null ){ boolean release = lock.token == this; if( release ){ lock.token = null; } if( dockable != null ){ if( link ){ lock.ensureLinked( station, dockable ); } else{ lock.ensureUnlinked( station, dockable ); } } if( release ){ lock.runOnRelease(); } } } } /** * Releases this lock without doing the usual checks. */ public void releaseNoCheck(){ synchronized( this ){ if( lock != null ){ lock.token = null; } } } } }