/* * 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.layout; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import bibliothek.gui.DockController; import bibliothek.gui.DockStation; import bibliothek.gui.Dockable; import bibliothek.gui.dock.DefaultDockable; import bibliothek.gui.dock.DockElement; import bibliothek.gui.dock.DockFactory; import bibliothek.gui.dock.FlapDockStation; import bibliothek.gui.dock.SplitDockStation; import bibliothek.gui.dock.StackDockStation; import bibliothek.gui.dock.dockable.DefaultDockableFactory; import bibliothek.gui.dock.perspective.Perspective; import bibliothek.gui.dock.perspective.PerspectiveElement; import bibliothek.gui.dock.station.flap.FlapDockStationFactory; import bibliothek.gui.dock.station.screen.ScreenDockStationFactory; import bibliothek.gui.dock.station.split.SplitDockStationFactory; import bibliothek.gui.dock.station.stack.StackDockStationFactory; import bibliothek.gui.dock.station.support.PlaceholderStrategy; import bibliothek.gui.dock.util.DockUtilities; import bibliothek.gui.dock.util.extension.ExtensionManager; import bibliothek.gui.dock.util.extension.ExtensionName; import bibliothek.util.Path; import bibliothek.util.Todo; import bibliothek.util.Version; import bibliothek.util.Todo.Compatibility; import bibliothek.util.Todo.Priority; import bibliothek.util.xml.XAttribute; import bibliothek.util.xml.XElement; import bibliothek.util.xml.XException; /** * A <code>DockSituation</code> is a converter: the relationship of {@link DockStation}s and {@link Dockable}s, * the position of <code>Dockable</code>s and other information are converted into a * stream of bytes. The other direction, read a stream and create <code>Dockable</code>s and <code>DockStation</code>s, is also possible.<br> * @author Benjamin Sigg */ public class DockSituation { /** Name for an {@link ExtensionName} to load additional {@link DockFactory}s */ public static final Path DOCK_FACTORY_EXTENSION = new Path("dock.DockSituation.DockFactory"); /** Name for an {@link ExtensionName} to load additional {@link AdjacentDockFactory}s */ public static final Path ADJACENT_DOCK_FACTORY_EXTENSION = new Path("dock.DockSituation.AdjacentDockFactory"); /** Name of a parameter of an {@link ExtensionName} pointing to <code>this</code> */ public static final String EXTENSION_PARAM = "situation"; /** the factories used to create new {@link DockElement elements}*/ private Map<String, DockFactory<?,?,?>> factories = new HashMap<String, DockFactory<?,?,?>>(); /** the factory used when no {@link DockFactory} is available */ private MissingDockFactory missingFactory; /** a set of additional factories for the {@link DockElement}s */ private Map<String, AdjacentDockFactory<?>> adjacent = new HashMap<String, AdjacentDockFactory<?>>(); /** the factory used when no {@link AdjacentDockFactory} is available */ private MissingDockFactory missingAdjacent; /** a filter for elements which should be ignored */ private DockSituationIgnore ignore; /** strategy used to filter placeholders in the intermediate format */ private PlaceholderStrategy intermediatePlaceholders; /** strategy used to filter placeholders when converting the intermediate format to real {@link DockElement}s */ private PlaceholderStrategy placeholders; /** * Constructs a new DockSituation and sets some factories which are * used to create new {@link DockElement DockElements}. Please note that this * constructor does not add the default factories, hence it should be used with care. * @param factories the factories */ public DockSituation( DockFactory<?,?,?>...factories ){ for( DockFactory<?,?,?> factory : factories ) this.factories.put( getID( factory ), factory ); } /** * Constructs a new DockSituation. Factories for {@link DefaultDockable}, * {@link SplitDockStation}, {@link StackDockStation} and * {@link FlapDockStation} will be preinstalled. * @param controller {@link DockController} in whose realm this {@link DockSituation} will be used, the * controller is used to access the {@link ExtensionManager} and load additional factories */ public DockSituation( DockController controller ){ this( new DefaultDockableFactory(), new SplitDockStationFactory(), new StackDockStationFactory(), new FlapDockStationFactory()); @SuppressWarnings("rawtypes") List<DockFactory> factories = controller.getExtensions().load( new ExtensionName<DockFactory>( DOCK_FACTORY_EXTENSION, DockFactory.class, EXTENSION_PARAM, this ) ); for( DockFactory<?,?,?> factory : factories ){ add( factory ); } @SuppressWarnings("rawtypes") List<AdjacentDockFactory> adjacent = controller.getExtensions().load( new ExtensionName<AdjacentDockFactory>( ADJACENT_DOCK_FACTORY_EXTENSION, AdjacentDockFactory.class, EXTENSION_PARAM, this ) ); for( AdjacentDockFactory<?> factory : adjacent ){ addAdjacent( factory ); } } /** * Creates a new {@link Perspective} that uses the settings made on this {@link DockSituation}. Changes on the * properties of this {@link DockSituation} will be noticed and used by the created {@link Perspective}. However * changes on the properties of the {@link Perspective} will not influence this {@link DockSituation}.<br> * Note that subclasses may create {@link Perspective}s that need the client to make additional settings before * it can be used. * @return the new perspective */ public Perspective createPerspective(){ return new Perspective( this ){ @Override protected String getID( PerspectiveElement element ){ return DockSituation.this.getID( element ); } @Override protected DockFactory<?,?,?> getFactory( String id ){ return DockSituation.this.getFactory( id ); } }; } /** * Sets a filter which decides, which elements (stations and dockables) * are stored. * @param ignore the filter or <code>null</code> */ public void setIgnore( DockSituationIgnore ignore ) { this.ignore = ignore; } /** * Gets the filter which decides, which elements are stored. * @return the filter or <code>null</code> */ public DockSituationIgnore getIgnore() { return ignore; } /** * Sets a strategy for deleting invalid placeholders. * @param placeholders the strategy, <code>null</code> for keeping all placeholders */ public void setPlaceholderStrategy( PlaceholderStrategy placeholders ){ this.placeholders = placeholders; } /** * Gets the current strategy for removing invalid placeholders. * @return the strategy, may be <code>null</code> */ public PlaceholderStrategy getPlaceholderStrategy(){ return placeholders; } /** * Sets the strategy for deleting invalid placeholders in the intermediate format * @param intermediatePlaceholders the strategy, can be <code>null</code> */ public void setIntermediatePlaceholders( PlaceholderStrategy intermediatePlaceholders ){ this.intermediatePlaceholders = intermediatePlaceholders; } /** * Gets the strategy for deleting invalid placeholders in the intermediate format. * @return the intermediate strategy, can be <code>null</code> */ public PlaceholderStrategy getIntermediatePlaceholders(){ return intermediatePlaceholders; } /** * Gets a placeholder for <code>element</code> using the current {@link PlaceholderStrategy}. * @param element some element, not <code>null</code> * @return the placeholder, can be <code>null</code> */ protected Path getPlaceholder( DockElement element ){ if( placeholders == null ){ return null; } Dockable dockable = element.asDockable(); if( dockable == null ){ return null; } return placeholders.getPlaceholderFor( dockable ); } /** * Adds a factory * @param factory the additional factory */ public void add( DockFactory<?,?,?> factory ){ factories.put( getID( factory ), factory ); } /** * Adds an adjacent factory * @param factory the new factory */ public void addAdjacent( AdjacentDockFactory<?> factory ){ adjacent.put( getAdjacentID( factory ), factory ); } /** * Sets a factory which is used whenever no ordinary {@link DockFactory} * can be found to read something. The <code>missingFactory</code> can convert * the input to any {@link Object} it likes, but if a missing factory * is later added to this situation, then that object needs to be casted * into the object used by the original factory. So when working with * a {@link MissingDockFactory} handling different types of layout-data * needs to be done very carefully. Note that this factory is only used to convert * byte-stream or xml data to the intermediate format. It cannot create any {@link Dockable} or * {@link DockElement} nor store any data. * @param missingFactory the factory, can be <code>null</code> */ public void setMissingFactory( MissingDockFactory missingFactory ) { this.missingFactory = missingFactory; } /** * Gets the factory which is used when another factory is missing. * @return the factory replacing missing factories, can be <code>null</code> * @see #setMissingFactory(MissingDockFactory) */ public MissingDockFactory getMissingFactory() { return missingFactory; } /** * Sets a factory which is used when a {@link AdjacentDockFactory} is missing. * There are the same issues with this factory than with the one used * by {@link #setMissingFactory(MissingDockFactory)}. * @param missingAdjacent the new factory, can be <code>null</code> * @see #setMissingFactory(MissingDockFactory) */ public void setMissingAdjacentFactory( MissingDockFactory missingAdjacent ) { this.missingAdjacent = missingAdjacent; } /** * Gets the factory which is used when another {@link AdjacentDockFactory} * is missing. * @return the factory, can be <code>null</code> * @see #setMissingAdjacentFactory(MissingDockFactory) */ public MissingDockFactory getMissingAdjacentFactory() { return missingAdjacent; } /** * Converts the layout of <code>element</code> and all its children into a * {@link DockLayoutComposition}. * @param element the element to convert * @return the composition or <code>null</code> if the element is ignored * @throws IllegalArgumentException if one element has an unknown id of * a {@link DockFactory}. * @throws ClassCastException if an element does not specify the correct * {@link DockFactory}. */ @SuppressWarnings("unchecked") public DockLayoutComposition convert( DockElement element ){ if( ignoreElement( element )) return null; String id = getID( element ); DockFactory<DockElement,?,Object> factory = (DockFactory<DockElement,?,Object>)getFactory( id ); if( factory == null ) throw new IllegalArgumentException( "Unknown factory-id: " + element.getFactoryID() ); DockStation station = element.asDockStation(); Map<Dockable, Integer> ids = new HashMap<Dockable, Integer>(); List<DockLayoutComposition> children = new ArrayList<DockLayoutComposition>(); boolean ignore = false; if( station != null ){ ignore = ignoreChildren( station ); if( !ignore ){ int index = 0; for( int i = 0, n = station.getDockableCount(); i<n; i++ ){ Dockable dockable = station.getDockable( i ); DockLayoutComposition composition = convert( dockable ); if( composition != null ){ children.add( composition ); ids.put( dockable, index++ ); } } } } Object data = factory.getLayout( element, ids ); DockLayout<Object> layout = new DockLayout<Object>( id, data ); List<DockLayout<?>> adjacent = null; for( AdjacentDockFactory<?> adjacentFactory : this.adjacent.values() ){ if( adjacentFactory.interested( element )){ Object adjacentData = adjacentFactory.getLayout( element, ids ); if( adjacent == null ){ adjacent = new ArrayList<DockLayout<?>>(); } adjacent.add( new DockLayout<Object>( getAdjacentID( adjacentFactory ), adjacentData ) ); } } DockLayoutInfo info = new DockLayoutInfo( layout ); info.setPlaceholder( getPlaceholder( element ) ); return new DockLayoutComposition( info, adjacent, children, ignore ); } /** * Reads the contents of <code>composition</code> and tries to create a * {@link DockElement} that matches the composition. * @param composition the composition to analyze * @return the new element, can be something else then a {@link DockElement} * if the factory for <code>composition</code> was not found */ @SuppressWarnings("unchecked") public DockElement convert( DockLayoutComposition composition ){ DockLayoutInfo info = composition.getLayout(); if( info == null ) return null; DockLayout<?> layout = info.getDataLayout(); if( layout == null ) return null; DockFactory<DockElement,?,Object> factory = (DockFactory<DockElement,?,Object>)getFactory( layout.getFactoryID() ); if( factory == null ) return null; DockElement result = null; Map<Integer, Dockable> children = null; if( composition.isIgnoreChildren() ){ for( DockLayoutComposition childComposition : composition.getChildren() ){ convert( childComposition ); } result = factory.layout( layout.getData(), placeholders ); } else{ children = new HashMap<Integer, Dockable>(); int index = 0; for( DockLayoutComposition childComposition : composition.getChildren() ){ DockElement child = convert( childComposition ); if( child != null ){ Dockable dockable = child.asDockable(); if( dockable != null ){ children.put( index, dockable ); if( dockable.getDockParent() != null ){ dockable.getDockParent().drag( dockable ); } } } index++; } result = factory.layout( layout.getData(), children, placeholders ); } if( result != null ){ List<DockLayout<?>> adjacent = composition.getAdjacent(); if( adjacent != null ){ for( DockLayout<?> adjacentLayout : adjacent ){ AdjacentDockFactory<Object> adjacentFactory = (AdjacentDockFactory<Object>)getAdjacentFactory( adjacentLayout.getFactoryID() ); if( adjacentFactory != null ){ if( children == null ){ adjacentFactory.setLayout( result, adjacentLayout.getData(), placeholders ); } else{ adjacentFactory.setLayout( result, adjacentLayout.getData(), children, placeholders ); } } } } } return result; } /** * Writes the contents of <code>composition</code> and all its children * to <code>out</code>. * @param composition the composition to write, should be created by * <code>this</code> {@link DockSituation} or a <code>DockSituation</code> with * similar properties. * @param out the stream to write into * @throws IOException if an I/O-error occurs */ public void writeComposition( DockLayoutComposition composition, DataOutputStream out ) throws IOException{ Version.write( out, Version.VERSION_1_0_8 ); writeCompositionStream( composition, out ); } /** * Writes the contents of <code>composition</code> and all its children * to <code>out</code>. * @param composition the composition to write, should be created by * <code>this</code> {@link DockSituation} or a <code>DockSituation</code> with * similar properties. * @param out the stream to write into * @throws IOException if an I/O-error occurs */ @SuppressWarnings("unchecked") private void writeCompositionStream( DockLayoutComposition composition, DataOutputStream out ) throws IOException{ DockLayoutInfo info = composition.getLayout(); // placeholder Path placeholder = info.getPlaceholder(); out.writeBoolean( placeholder != null ); if( placeholder != null ){ out.writeUTF( placeholder.toString() ); } if( info.getKind() == DockLayoutInfo.Data.BYTE ){ // data out.write( info.getDataByte() ); } else if( info.getKind() == DockLayoutInfo.Data.DOCK_LAYOUT ){ DockLayout<?> layout = info.getDataLayout(); DockFactory<DockElement,?,Object> factory = (DockFactory<DockElement,?,Object>)getFactory( layout.getFactoryID() ); if( factory == null ) throw new IOException( "Missing factory: " + layout.getFactoryID() ); // factory out.writeUTF( getID( factory ) ); // contents ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream( bout ); factory.write( layout.getData(), dout ); dout.close(); out.writeInt( bout.size() ); bout.writeTo( out ); } else{ // there is nothing to write... throw new IllegalArgumentException( "Cannot store layout in stream: it was never translated and its raw format is not a byte stream" ); } // adjacent List<DockLayout<?>> adjacentLayouts = composition.getAdjacent(); if( adjacentLayouts == null ){ out.writeInt( 0 ); } else{ out.writeInt( adjacentLayouts.size() ); for( DockLayout<?> adjacentLayout : adjacentLayouts ){ AdjacentDockFactory<Object> adjacentFactory = (AdjacentDockFactory<Object>)getAdjacentFactory( adjacentLayout.getFactoryID() ); if( adjacentFactory == null ) throw new IOException( "Missing adjacent factory: " + adjacentLayout.getFactoryID() ); ByteArrayOutputStream adjacentBOut = new ByteArrayOutputStream(); DataOutputStream adjacentOut = new DataOutputStream( adjacentBOut ); adjacentFactory.write( adjacentLayout.getData(), adjacentOut ); adjacentOut.close(); out.writeUTF( getAdjacentID( adjacentFactory ) ); out.writeInt( adjacentBOut.size() ); adjacentBOut.writeTo( out ); } } // ignore out.writeBoolean( composition.isIgnoreChildren() ); // children List<DockLayoutComposition> children = composition.getChildren(); out.writeInt( children.size() ); for( DockLayoutComposition child : children ){ writeCompositionStream( child, out ); } } /** * Reads one {@link DockLayoutComposition} and all its children. * @param in the stream to read from * @return the new composition or <code>null</code> if the factory was missing * @throws IOException if an I/O-error occurs */ public DockLayoutComposition readComposition( DataInputStream in ) throws IOException{ Version version = Version.read( in ); version.checkCurrent(); return readCompositionStream( in, version ); } /** * Reads one {@link DockLayoutComposition} and all its children. * @param in the stream to read from * @param version the format of <code>in</code> * @return the new composition or <code>null</code> if the factory was missing * @throws IOException if an I/O-error occurs */ @SuppressWarnings("unchecked") private DockLayoutComposition readCompositionStream( DataInputStream in, Version version ) throws IOException{ // factory Path entryPlaceholder = null; if( Version.VERSION_1_0_8.compareTo( version ) <= 0 ){ if( in.readBoolean() ){ entryPlaceholder = new Path( in.readUTF() ); } } byte[] entry = readBuffer( in ); DockLayoutInfo info = readEntry( entry, entryPlaceholder ); List<DockLayout<?>> adjacentLayouts = null; if( Version.VERSION_1_0_7.compareTo( version ) <= 0 ){ // adjacent int layoutCount = in.readInt(); if( layoutCount > 0 ){ adjacentLayouts = new ArrayList<DockLayout<?>>( layoutCount ); for( int i = 0; i < layoutCount; i++ ){ String adjacentFactoryId = in.readUTF(); int adjacentCount = in.readInt(); AdjacentDockFactory<Object> adjacentFactory = (AdjacentDockFactory<Object>)getAdjacentFactory( adjacentFactoryId ); if( adjacentFactory == null ){ if( missingAdjacent == null ){ // skip while( adjacentCount > 0 ){ int skipped = (int)in.skip( adjacentCount ); if( skipped <= 0 ) throw new EOFException(); adjacentCount -= skipped; } } else{ DataInputStream din = readBuffer( in, adjacentCount ); Object data = missingAdjacent.read( getAdjacentFactoryID( adjacentFactoryId ), din, adjacentCount ); if( data != null ){ adjacentLayouts.add( new DockLayout<Object>( adjacentFactoryId, data ) ); } din.close(); } } else{ DataInputStream din = readBuffer( in, adjacentCount ); Object data = adjacentFactory.read( din, intermediatePlaceholders ); if( data != null ){ adjacentLayouts.add( new DockLayout<Object>( adjacentFactoryId, data ) ); } din.close(); } } } } // ignore boolean ignore = in.readBoolean(); // children List<DockLayoutComposition> children = new ArrayList<DockLayoutComposition>(); int count = in.readInt(); for( int i = 0; i < count; i++ ){ children.add( readCompositionStream( in, version ) ); } // result return new DockLayoutComposition( info, adjacentLayouts, children, ignore ); } /** * Tries to read <code>entry</code>. Entry will be read by a {@link DataInputStream}, * it must start with an utf-String used as id for a {@link DockFactory}, * then followed by an int telling how many bytes are in the remaining * array. The rest of the array will be given to a {@link DockFactory}. * @param entry the entry to read * @param placeholder the placeholder which is associated with this element * @return the information that was obtained, may be <code>null</code> * @throws IOException if <code>entry</code> has not the correct format */ @SuppressWarnings("unchecked") private DockLayoutInfo readEntry( byte[] entry, Path placeholder ) throws IOException{ DataInputStream entryIn = new DataInputStream( new ByteArrayInputStream( entry )); String factoryId = entryIn.readUTF(); DockFactory<DockElement,?,Object> factory = (DockFactory<DockElement,?,Object>)getFactory( factoryId ); // contents DockLayoutInfo info; int count = entryIn.readInt(); if( factory == null ){ // try read info = null; if( missingFactory != null ){ Object data = missingFactory.read( getFactoryID( factoryId ), entryIn, count ); entryIn.close(); if( data != null ){ info = new DockLayoutInfo( new DockLayout<Object>( factoryId, data )); info.setPlaceholder( placeholder ); } } if( info == null ){ info = new DockLayoutInfo( entry ); info.setPlaceholder( placeholder ); } } else{ Object data = factory.read( entryIn, intermediatePlaceholders ); if( data == null ){ info = null; } else{ info = new DockLayoutInfo( new DockLayout<Object>( factoryId, data ) ); info.setPlaceholder( placeholder ); } entryIn.close(); } return info; } /** * Reads <code>count</code> bytes from <code>in</code> and returns them * in a new stream. * @param in the stream to read from * @param count the number of bytes to read * @return a new stream with <code>count</code> bytes * @throws IOException if <code>in</code> cannot be read or does not * have <code>count</code> elements */ private DataInputStream readBuffer( DataInputStream in, int count ) throws IOException{ byte[] buffer = new byte[ count ]; int read = 0; while( read < count ){ int input = in.read( buffer, read, count-read ); if( input < 0 ) throw new EOFException(); read += input; } ByteArrayInputStream bin = new ByteArrayInputStream( buffer ); DataInputStream din = new DataInputStream( bin ); return din; } /** * Reads one entry of the stream and returns the whole entry. * An entry starts with an UTF-String, then an int telling how many bytes * follow, then an array of bytes. * @param in the stream to read from * @return the whole entry * @throws IOException if the entry cannot be read from <code>in</code> */ private byte[] readBuffer( DataInputStream in ) throws IOException{ String factory = in.readUTF(); int count = in.readInt(); ByteArrayOutputStream out = new ByteArrayOutputStream( factory.length()*4 + 4 + count ); DataOutputStream dout = new DataOutputStream( out ); dout.writeUTF( factory ); dout.writeInt( count ); for( int i = 0; i < count; i++ ){ int read = in.read(); if( read == -1 ) throw new EOFException( "unexpectetly reached end of file" ); dout.write( read ); } dout.close(); return out.toByteArray(); } /** * Writes all locations and relationships of the {@link DockStation DockStations} * <code>stations</code> and their children into an array of bytes. * @param stations The stations to store, a call to {@link #read(byte[])} * would return the same map. Only the roots are needed. * @return the information as an array of bytes * @throws IOException if the information can't be written */ public byte[] write( Map<String, DockStation> stations ) throws IOException{ ByteArrayOutputStream bytes = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream( bytes ); write( stations, out ); out.close(); return bytes.toByteArray(); } /** * Writes all locations and relationships of the {@link DockStation DockStations} * <code>stations</code> and their children into the stream <code>out</code>. * @param stations The stations to store, only the roots are needed. * @param out the stream to write in * @throws IOException if the stream throws an exception */ public void write( Map<String, DockStation> stations, DataOutputStream out ) throws IOException{ Map<String, DockLayoutComposition> map = new HashMap<String, DockLayoutComposition>(); for( Map.Entry<String, DockStation> entry : stations.entrySet() ){ DockLayoutComposition composition = convert( entry.getValue() ); if( composition != null ){ map.put( entry.getKey(), composition ); } } writeCompositions( map, out ); } /** * Writes all information stored in <code>stations</code> to <code>out</code>. * @param stations Representations of the root-stations. * @param out the stream to write in * @throws IOException if the stream throws an exception */ public void writeCompositions( Map<String, DockLayoutComposition> stations, DataOutputStream out ) throws IOException{ Version.write( out, Version.VERSION_1_0_4 ); out.writeInt( stations.size() ); for( Map.Entry<String, DockLayoutComposition> entry : stations.entrySet() ){ out.writeUTF( entry.getKey() ); writeComposition( entry.getValue(), out ); } } /** * Reads <code>data</code> as stream and returns the roots of the * {@link DockElement DockElements} which were found. * @param data the array to read * @return the root stations which were found * @throws IOException if <code>data</code> can't be read */ public Map<String, DockStation> read( byte[] data ) throws IOException{ DataInputStream in = new DataInputStream( new ByteArrayInputStream( data )); Map<String, DockStation> result = read( in ); in.close(); return result; } /** * Reads <code>in</code> and returns the roots of the * {@link DockElement DockElements} which were found. * @param in the stream to read * @return the roots of all elements that were found * @throws IOException if the stream can't be read */ public Map<String, DockStation> read( DataInputStream in ) throws IOException{ Version version = Version.read( in ); version.checkCurrent(); int count = in.readInt(); Map<String, DockStation> result = new HashMap<String, DockStation>(); for( int i = 0; i < count; i++ ){ String key = in.readUTF(); DockLayoutComposition composition = readComposition( in ); DockElement element = composition == null ? null : convert( composition ); DockStation station = element == null ? null : element.asDockStation(); if( station != null ){ result.put( key, station ); } } return result; } /** * Reads <code>in</code> and returns the map of {@link DockLayoutComposition}s that was * stored. * @param in the stream to read from * @return the roots of the layout * @throws IOException if an I/O-error occurs */ public Map<String, DockLayoutComposition> readCompositions( DataInputStream in ) throws IOException{ Version version = Version.read( in ); version.checkCurrent(); int count = in.readInt(); Map<String, DockLayoutComposition> result = new HashMap<String, DockLayoutComposition>(); for( int i = 0; i < count; i++ ){ String key = in.readUTF(); DockLayoutComposition composition = readComposition( in ); if( composition != null ){ result.put( key, composition ); } } return result; } /** * Writes the contents of <code>composition</code> into <code>element</code> without * changing the attributes of <code>element</code>. * @param composition the composition to write * @param element the element to write into * @throws IllegalArgumentException if a factory is missing */ @SuppressWarnings("unchecked") public void writeCompositionXML( DockLayoutComposition composition, XElement element ){ DockLayoutInfo info = composition.getLayout(); if( info.getKind() == DockLayoutInfo.Data.XML ){ element.addElement( info.getDataXML() ); } else if( info.getKind() == DockLayoutInfo.Data.DOCK_LAYOUT ){ DockLayout<?> layout = info.getDataLayout(); DockFactory<DockElement,?,Object> factory = (DockFactory<DockElement,?,Object>)getFactory( layout.getFactoryID() ); if( factory == null ) throw new IllegalArgumentException( "Missing factory: " + layout.getFactoryID() ); XElement xfactory = element.addElement( "layout" ); xfactory.addString( "factory", getID( factory ) ); Path placeholder = info.getPlaceholder(); if( placeholder != null ){ xfactory.addString( "placeholder", placeholder.toString() ); } factory.write( layout.getData(), xfactory ); } else{ // there is nothing to write... throw new IllegalArgumentException( "Cannot store layout as XML: it was never translated and its raw format is not XML" ); } List<DockLayout<?>> adjacentLayouts = composition.getAdjacent(); if( adjacentLayouts != null ){ XElement xadjacent = element.addElement( "adjacent" ); for( DockLayout<?> adjacentLayout : adjacentLayouts ){ AdjacentDockFactory<Object> adjacentFactory = (AdjacentDockFactory<Object>)getAdjacentFactory( adjacentLayout.getFactoryID() ); if( adjacentFactory == null ) throw new IllegalArgumentException( "Missing adjacent factory: " + adjacentLayout.getFactoryID() ); XElement xlayout = xadjacent.addElement( "layout" ); xlayout.addString( "factory", getAdjacentID( adjacentFactory ) ); adjacentFactory.write( adjacentLayout.getData(), xlayout ); } } XElement xchildren = element.addElement( "children" ); xchildren.addBoolean( "ignore", composition.isIgnoreChildren() ); for( DockLayoutComposition child : composition.getChildren() ){ XElement xchild = xchildren.addElement( "child" ); writeCompositionXML( child, xchild ); } } /** * Reads a {@link DockLayoutComposition} from an xml element. * @param element the element to read * @return the composition that was read * @throws XException if something is missing or malformed in <code>element</code> */ @SuppressWarnings("unchecked") public DockLayoutComposition readCompositionXML( XElement element ){ XElement xfactory = element.getElement( "layout" ); DockLayoutInfo layout = readEntry( xfactory ); XElement xadjacent = element.getElement( "adjacent" ); List<DockLayout<?>> adjacentLayouts = null; if( xadjacent != null ){ adjacentLayouts = new ArrayList<DockLayout<?>>(); for( XElement xlayout : xadjacent.getElements( "layout" )){ String factoryId = xlayout.getString( "factory" ); AdjacentDockFactory<Object> adjacentFactory = (AdjacentDockFactory<Object>)getAdjacentFactory( factoryId ); if( adjacentFactory != null ){ Object data = adjacentFactory.read( xlayout, intermediatePlaceholders ); if( data != null ){ adjacentLayouts.add( new DockLayout<Object>( factoryId, data )); } } else if( missingAdjacent != null ){ Object data = missingAdjacent.readXML( getAdjacentFactoryID( factoryId ), xlayout ); if( data != null ){ adjacentLayouts.add( new DockLayout<Object>( factoryId, data ) ); } } } } XElement xchildren = element.getElement( "children" ); boolean ignore = true; List<DockLayoutComposition> children = new ArrayList<DockLayoutComposition>(); if( xchildren != null ){ ignore = xchildren.getBoolean( "ignore" ); for( XElement xchild : xchildren.getElements( "child" )){ children.add( readCompositionXML( xchild )); } } return new DockLayoutComposition( layout, adjacentLayouts, children, ignore ); } /** * Reads an entry that was stored in xml format. The entry should have * one attribute <code>factory</code>. * @param element the element to read, can be <code>null</code> * @return the information of <code>element</code>, may be <code>null</code> */ @SuppressWarnings("unchecked") private DockLayoutInfo readEntry( XElement element ){ DockLayoutInfo layout = null; if( element != null ){ String factoryId = element.getString( "factory" ); Path placeholder = null; XAttribute xplaceholder = element.getAttribute( "placeholder" ); if( xplaceholder != null ){ placeholder = new Path( xplaceholder.getString() ); } DockFactory<DockElement,?,Object> factory = (DockFactory<DockElement,?,Object>)getFactory( factoryId ); if( factory != null ){ Object data = factory.read( element, intermediatePlaceholders ); if( data != null ){ layout = new DockLayoutInfo( new DockLayout<Object>( factoryId, data ) ); layout.setPlaceholder( placeholder ); } } else{ layout = null; if( missingFactory != null ){ Object data = missingFactory.readXML( getFactoryID( factoryId ), element ); if( data != null ){ layout = new DockLayoutInfo( new DockLayout<Object>( factoryId, data ) ); layout.setPlaceholder( placeholder ); } } if( layout == null ){ layout = new DockLayoutInfo( element ); layout.setPlaceholder( placeholder ); } } } return layout; } /** * Writes all locations and relationships of the {@link DockStation}s * <code>stations</code> and their children as xml. * @param stations The stations to store, only the roots are needed. * @param element the element to write into, attributes of <code>element</code> will * not be changed */ public void writeXML( Map<String, DockStation> stations, XElement element ) { Map<String, DockLayoutComposition> map = new HashMap<String, DockLayoutComposition>(); for( Map.Entry<String, DockStation> entry : stations.entrySet() ){ DockLayoutComposition composition = convert( entry.getValue() ); if( composition != null ){ map.put( entry.getKey(), composition ); } } writeCompositionsXML( map, element ); } /** * Writes the contents of <code>station</code> into <code>element</code>. * @param stations the items to write * @param element the element to write into, the attributes of <code>element</code> * will not be changed */ public void writeCompositionsXML( Map<String, DockLayoutComposition> stations, XElement element ) { for( Map.Entry<String, DockLayoutComposition> entry : stations.entrySet() ){ XElement xchild = element.addElement( "element" ); xchild.addString( "name", entry.getKey() ); writeCompositionXML( entry.getValue(), xchild ); } } /** * Reads a set of {@link DockStation}s that were stored earlier. * @param root the xml element from which to read * @return the set of station */ public Map<String, DockStation> readXML( XElement root ){ Map<String, DockStation> result = new HashMap<String, DockStation>(); for( XElement xelement : root.getElements( "element" )){ String name = xelement.getString( "name" ); DockLayoutComposition composition = readCompositionXML( xelement ); DockElement element = composition == null ? null : convert( composition ); DockStation station = element == null ? null : element.asDockStation(); if( station != null ) result.put( name, station ); } return result; } /** * Reads a set of {@link DockLayoutComposition}s that were stored earlier. * @param root the xml element from which to read * @return the set of compositions */ public Map<String, DockLayoutComposition> readCompositionsXML( XElement root ){ Map<String, DockLayoutComposition> result = new HashMap<String, DockLayoutComposition>(); for( XElement xelement : root.getElements( "element" )){ String name = xelement.getString( "name" ); DockLayoutComposition composition = readCompositionXML( xelement ); if( composition != null ){ result.put( name, composition ); } } return result; } /** * Using the factories currently known to this {@link DockSituation}, this * method tries to fill gaps in <code>composition</code>. It checks * all the {@link DockLayoutInfo}s, if an info contains a byte array or * an {@link XElement}, then this method tries to use a factory to * read the element. If a {@link #setMissingFactory(MissingDockFactory) missing factory} * is present, then this factory is used as well. * @param composition the composition to read * @return either <code>composition</code> or a new composition if this * method changed something * @throws IOException if some stream was opened but cannot be read * @throws XException if some xml element was found but cannot be read */ public DockLayoutComposition fillMissing( DockLayoutComposition composition ) throws IOException, XException{ DockLayoutInfo info = composition.getLayout(); DockLayoutInfo original = info; if( info.getKind() == DockLayoutInfo.Data.BYTE ){ info = readEntry( info.getDataByte(), info.getPlaceholder() ); if( info != null && info.getKind() == DockLayoutInfo.Data.BYTE ){ info = original; } } else if( info.getKind() == DockLayoutInfo.Data.XML ){ info = readEntry( info.getDataXML() ); if( info != null && info.getKind() == DockLayoutInfo.Data.XML ){ info = original; } } if( info.getKind() == DockLayoutInfo.Data.DOCK_LAYOUT ){ info = fillMissing( info ); } boolean createNew = info != original; List<DockLayoutComposition> children = composition.getChildren(); List<DockLayoutComposition> newChildren; if( children != null ){ newChildren = new ArrayList<DockLayoutComposition>( children.size() ); for( DockLayoutComposition child : children ){ DockLayoutComposition filled = fillMissing( child ); newChildren.add( filled ); if( child != filled ){ createNew = true; } } } else{ newChildren = null; } if( info != null && info != original ){ info.setLocation( original.getLocation() ); } if( createNew ){ return new DockLayoutComposition( info, composition.getAdjacent(), newChildren, composition.isIgnoreChildren() ); } return composition; } /** * Called by {@link #fillMissing(DockLayoutComposition)} only for * {@link DockLayoutInfo}s which contain a {@link DockLayout}. This method * can apply further updates to <code>info</code> if necessary. The default * implementation just returns <code>info</code>.<br> * This method is intended for subclasses which wrap {@link DockFactory}s * and use a {@link DockLayout} even for incomplete data. * @param info the info to update * @return either <code>info</code> if nothing changed or a new * {@link DockLayoutInfo} if additional information could be retrieved */ protected DockLayoutInfo fillMissing( DockLayoutInfo info ){ return info; } /** * Tries to guess the location of the elements stored in the tree * below <code>composition</code>. Invoking this method is the same as calling * <code>guessLocation( composition, composition.getLayout().getLocation() );</code>. * @param composition the composition whose location will be determined */ public void estimateLocations( DockLayoutComposition composition ){ estimateLocations( composition, composition.getLayout().getLocation() ); } /** * Tries to guess the location of the elements stored in the tree below * <code>composition</code>, assuming that <code>composition</code> itself * is at location <code>location</code>. This method reads out the * {@link DockLayoutInfo} through {@link DockLayoutComposition#getLayout()} * and then calls {@link DockLayoutInfo#setLocation(DockableProperty)}, so * <code>composition</code> gets modified by this method. This method stops * its recursion if the location of a child of <code>composition</code> * was not found.<br> * This method returns immediately if one of: * <ul> * <li><code>composition</code> does not have children</li> * <li><code>composition</code> does not carry a {@link DockLayout}</li> * <li>There is no {@link DockFactory} registered for the factory-id found * in <code>composition</code></li> * </ul> <br> * Note: if the number of factories changed, then it might be a good idea * to call {@link #fillMissing(DockLayoutComposition)} before invoking this method. * @param composition the composition whose children should be analyzed * @param location the location of <code>composition</code>, can be <code>null</code> */ public void estimateLocations( DockLayoutComposition composition, DockableProperty location ){ DefaultLocationEstimationMap map = new DefaultLocationEstimationMap( composition ); estimateLocations( map ); if( location != null ){ appendFirstOnEstimate( composition, location ); } } private void appendFirstOnEstimate( DockLayoutComposition composition, DockableProperty location ){ DockLayoutInfo info = composition.getLayout(); DockableProperty property = info.getLocation(); if( property != null ){ info.setLocation( DockUtilities.append( property, location ) ); } for( DockLayoutComposition child : composition.getChildren() ){ appendFirstOnEstimate( child, location ); } } /** * Recursively tries to estimate the locations of all {@link DockLayoutInfo}s that can * be found in <code>map</code>.<br> * <b>Note:</b> this method does <i>not</i> call {@link DefaultLocationEstimationMap#finish()}. * @param map the root of the tree for which locations need to be estimated */ @SuppressWarnings("unchecked") protected void estimateLocations( DefaultLocationEstimationMap map ){ DockLayoutComposition composition = map.getRoot(); List<DockLayoutComposition> children = composition.getChildren(); if( children == null || children.size() == 0 ){ return; } DockLayout<Object> layout = (DockLayout<Object>)composition.getLayout().getDataLayout(); if( layout == null ){ return; } DockFactory<DockElement,?,Object> factory = (DockFactory<DockElement,?,Object>)getFactory( layout.getFactoryID() ); if( factory == null ){ return; } for( int i = 0, n = map.getChildCount(); i<n; i++ ){ estimateLocations( map.subMap( i ) ); } map.prepare(); factory.estimateLocations( layout.getData(), map ); map.finish(); } /** * Tells whether to ignore this element when saving. If an element is ignored, no * factory is needed for it. This implementation forwards * the call to the {@link DockSituationIgnore} of this situation. * @param element the element which might not be saved * @return <code>true</code> if the element should not be saved */ protected boolean ignoreElement( DockElement element ){ if( ignore == null ) return false; return ignore.ignoreElement( element ); } /** * Tells whether to ignore the children of the station when saving or not. If the children * are ignored, no factories are needed for them. This implementation forwards * the call to the {@link DockSituationIgnore} of this situation. * @param station the station whose children might be ignored * @return <code>true</code> if the station is saved as having no children */ protected boolean ignoreChildren( DockStation station ){ if( ignore == null ) return false; return ignore.ignoreChildren( station ); } /** * Gets the id of the factory which is needed to write (and later * read) <code>element</code> * @param element the element to read * @return the id of the factory * @see #getID(DockFactory) * @see #getFactory(String) */ protected String getID( PerspectiveElement element ){ return element.getFactoryID(); } /** * Gets the id of the factory which is needed to write (and later * read) <code>element</code> * @param element the element to read * @return the id of the factory * @see #getID(DockFactory) * @see #getFactory(String) */ protected String getID( DockElement element ){ return element.getFactoryID(); } /** * Gets the id of <code>factory</code>. The default behavior is just to * return {@link DockFactory#getID()}. Note that this method should be * a bijection to {@link #getFactory(String)}. * @param factory the factory whose id is needed * @return the id of the factory */ protected String getID( DockFactory<?,?,?> factory ){ return factory.getID(); } /** * Reads the id of the factory which was used to create <code>info</code>. * This returns the id of the factory as it is used internally by this * {@link DockSituation}. * @param info the info to check * @return the id of a factory or <code>null</code> if info does not contain * data * @throws IllegalArgumentException if the data of <code>info</code> is in the wrong format * @throws XException if the data of <code>info</code> is in the wrong format */ protected String getFactoryID( DockLayoutInfo info ){ if( info.getKind() == DockLayoutInfo.Data.BYTE ){ try{ DataInputStream in = new DataInputStream( new ByteArrayInputStream( info.getDataByte() )); String result = in.readUTF(); in.close(); return result; } catch( IOException ex ){ throw new IllegalArgumentException( "byte entry not in the correct format", ex ); } } if( info.getKind() == DockLayoutInfo.Data.XML ){ return info.getDataXML().getString( "factory" ); } if( info.getKind() == DockLayoutInfo.Data.DOCK_LAYOUT ){ return info.getDataLayout().getFactoryID(); } return null; } /** * Tells what identifier is used for <code>factory</code> in the * {@link DockLayoutComposition}.<br> * This method just calls {@link #getID(DockFactory)}, but * {@link #getID(DockFactory)} is intended for internal use while this * method is intended to be used by clients which read out a {@link DockLayoutComposition}. * @param factory the factory which might be used * @return the identifier * @see #getID(DockFactory) */ public String convertFactoryId( DockFactory<?,?,?> factory ){ return getID( factory ); } /** * Tells what identifier the {@link DockFactory} has, for which the * identifier <code>id</code> is used within a {@link DockLayoutComposition}.<br> * This method just calls {@link #getFactoryID(String)}, but while * {@link #getFactoryID(String)} is intended for internal use, this method * is intended for clients. * @param id an identifier found in a {@link DockLayoutComposition} * @return the identifer of a {@link DockFactory} */ public String convertFactoryId( String id ){ return getFactoryID( id ); } /** * Transforms an id read from a stream to the id of the factory which * would be used. This method must fulfill one contract: * <code>DockFactory factory = ... * factory.getID().equals( getFactoryID( getID( factory )));</code> * @param id the id read from a stream * @return the id of the original factory */ protected String getFactoryID( String id ){ return id; } /** * Gets the id of <code>factory</code>. The default behavior is just to * return {@link DockFactory#getID()}. Note that this method should be * a bijection to {@link #getAdjacentFactory(String)}. * @param factory the factory whose id is needed * @return the id of the factory */ protected String getAdjacentID( AdjacentDockFactory<?> factory ){ return factory.getID(); } /** * Transforms an id read from a stream to the id of the adjacent factory which * would be used. This method must fulfill one contract: * <code>AdjacentDockFactory factory = ... * factory.getID().equals( getFactoryID( getAdjacentID( factory )));</code> * @param id the id read from a stream * @return the id of the original factory */ protected String getAdjacentFactoryID( String id ){ return id; } /** * Gets the factory which has the given <code>id</code>. Note that this * method should be a bijection to {@link #getID(DockFactory)}. The * default behavior compares <code>id</code> with the * {@link #getID(DockFactory)}. * @param id the name of the factory * @return the factory or <code>null</code> if no factory has this id */ @Todo( compatibility=Compatibility.BREAK_MINOR, priority=Priority.MAJOR, target=Todo.Version.VERSION_1_1_3, description="remove the legacy code that filters out identifiers that look like 'secure ...'. Instead 'result' can be returned directly." ) public DockFactory<? extends DockElement,?,?> getFactory( String id ){ DockFactory<?, ?, ?> result = factories.get( id ); if( result == null ){ String base = null; if( id.startsWith( "delegate_secure " )){ base = id.substring( "delegate_secure ".length() ); id = "delegate_" + base; } else if( id.startsWith( "secure " )){ base = id.substring( "secure ".length() ); id = base; } if( FlapDockStationFactory.ID.equals( base) || ScreenDockStationFactory.ID.equals( base ) || StackDockStationFactory.ID.equals( base ) || SplitDockStationFactory.ID.equals( base )){ result = factories.get( id ); } } return result; } /** * Gets the adjacent factory which has the given <code>id</code>. Note that this * method should be a bijection to {@link #getID(DockFactory)}. The * default behavior compares <code>id</code> with the * {@link #getID(DockFactory)}. * @param id the name of the factory * @return the factory or <code>null</code> if no factory has this id */ public AdjacentDockFactory<?> getAdjacentFactory( String id ){ return adjacent.get( id ); } /** * Gets all the adjacent factories that are currently registered at this {@link DockSituation}, * the returned {@link Map} is unmodifiable. * @return an unmodifiable map containing all {@link AdjacentDockFactory}s. */ public Map<String, AdjacentDockFactory<?>> getAdjacentFactorys(){ return Collections.unmodifiableMap( adjacent ); } /** * Tells what identifier was associated with <code>composition</code> when it was * stored by a {@link DockSituation} of this type. The default implementation always * returns <code>null</code>. * @param composition some element that was created by this or a similar {@link DockSituation} * @return the identifier that was associated with <code>composition</code> */ public String getIdentifier( DockLayoutComposition composition ){ return null; } }