/*
* 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) 2008 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.extension.gui.dock.preference;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import bibliothek.util.Path;
import bibliothek.util.Version;
import bibliothek.util.xml.XElement;
import bibliothek.util.xml.XException;
/**
* A preference storage is a container storing the values of artificial preferences of one or more {@link PreferenceModel}s.
* Clients can use {@link #store(PreferenceModel)} to transfer values from model to storage, or
* {@link #load(PreferenceModel, boolean)} to transfer values from storage to model. Further mode the entire storage
* can be persistently written to a file using one of the {@link #writeXML(XElement) write-methods}. It can later be loaded
* using one of the {@link #readXML(XElement) read-methods}.<br>
* The static {@link #readXML(PreferenceModel, XElement) read-methods} and the static {@link #writeXML(PreferenceModel, XElement) write-methods}
* can be used to write and read the contents of a {@link PreferenceModel} directly.
* @author Benjamin Sigg
*/
public class PreferenceStorage {
/**
* Writes the current preferences of <code>model</code> into <code>out</code>.
* @param model the model to store
* @param out the stream to write into
* @throws IOException if the stream is not writeable
*/
public static void write( PreferenceModel model, DataOutputStream out ) throws IOException{
PreferenceStorage storage = new PreferenceStorage();
storage.store( model );
storage.write( out );
}
/**
* Reads preferences from <code>in</code> and transfers them into <code>model</code>.
* Missing preferences will be replaced by <code>null</code>.
* @param model the model to write into.
* @param in the stream to read from
* @throws IOException if the stream cannot be read
*/
public static void read( PreferenceModel model, DataInputStream in ) throws IOException{
PreferenceStorage storage = new PreferenceStorage();
storage.read( in );
storage.load( model, true );
}
/**
* Writes the preferences of <code>model</code> into <code>element</code>. This
* method will add new children to <code>element</code> but not change
* its attributes.
* @param model the model to store
* @param element the element to write into
*/
public static void writeXML( PreferenceModel model, XElement element ) {
PreferenceStorage storage = new PreferenceStorage();
storage.store( model );
storage.writeXML( element );
}
/**
* Reads some preferences from <code>element</code> and stores them in
* <code>model</code>.
* @param model the model to write into
* @param element the element to read
* @throws XException if <code>element</code> is incorrect
*/
public static void readXML( PreferenceModel model, XElement element ){
PreferenceStorage storage = new PreferenceStorage();
storage.readXML( element );
storage.load( model, true );
}
/** the available factories */
private Map<Path, PreferenceFactory<?>> factories = new HashMap<Path, PreferenceFactory<?>>();
/** the root of all nodes */
private Node root = new Node( null );
/**
* Creates a new preference storage with some default factories set.
*/
public PreferenceStorage(){
addFactory( Path.TYPE_INT_PATH, PreferenceFactory.FACTORY_INT );
addFactory( Path.TYPE_STRING_PATH, PreferenceFactory.FACTORY_STRING );
addFactory( Path.TYPE_BOOLEAN_PATH, PreferenceFactory.FACTORY_BOOLEAN );
addFactory( Path.TYPE_KEYSTROKE_PATH, PreferenceFactory.FACTORY_KEYSTROKE );
addFactory( Path.TYPE_MODIFIER_MASK_PATH, PreferenceFactory.FACTORY_MODIFIER_MASK );
addFactory( Path.TYPE_STRING_CHOICE_PATH, PreferenceFactory.FACTORY_STRING );
}
/**
* Adds a new factory to this storage, the factory will be responsible to
* write or read some kind of preferences. If there is already a factory
* for <code>type</code>, then the old factory will be replaced by
* <code>factory</code>
* @param type the type of values <code>factory</code> handles, this path
* is most times just the name of some class. Node: there is a set of
* standard paths defined in {@link Path}
* @param factory the new factory
*/
public void addFactory( Path type, PreferenceFactory<?> factory ){
if( type == null )
throw new IllegalArgumentException( "type must not be null" );
if( factory == null )
throw new IllegalArgumentException( "factory must not be null" );
if( type.getSegmentCount() == 0 )
throw new IllegalArgumentException( "the root path is not a valid path for this metho" );
factories.put( type, factory );
}
/**
* Stores all the preferences of <code>model</code> in this storage. This
* storage uses the {@link PreferenceModel#getPath(int) paths} of the
* preferences to store each value individually. If there is already a value
* for some path, then that value gets replaced.
* @param model the model to read out
*/
public void store( PreferenceModel model ){
for( int i = 0, n = model.getSize(); i<n; i++ ){
if( !model.isNatural( i )){
Node node = root.getNode( model.getPath( i ), true );
node.put( model.getTypePath( i ), model.getValue( i ) );
}
}
}
/**
* Gets through all the preferences of <code>model</code> and changes
* their values according to the values stored in this storage.
* @param missingToNull whether missing values should be set to <code>null</code>. If
* not set, then missing values remain just unchanged.
* @param model the model to write into
*/
public void load( PreferenceModel model, boolean missingToNull ){
for( int i = 0, n = model.getSize(); i<n; i++ ){
if( !model.isNatural( i )){
Node node = root.getNode( model.getPath( i ), false );
if( node == null ){
if( missingToNull ){
model.setValue( i, null );
}
}
else{
model.setValue( i, node.value );
}
}
else{
model.setValueNatural( i );
}
}
}
/**
* Writes all values currently stored in this storage to <code>out</code>.
* @param out the stream to write into
* @throws IOException if the stream is not writable or if there is a
* factory missing for some type
*/
public void write( DataOutputStream out ) throws IOException{
Version.write( out, Version.CURRENT );
write( root, out );
}
@SuppressWarnings("unchecked")
private void write( Node node, DataOutputStream out ) throws IOException{
Path type = node.getType();
if( type == null ){
out.writeBoolean( false );
}
else{
out.writeBoolean( true );
out.writeUTF( type.toString() );
Object value = node.getValue();
if( value == null ){
out.writeBoolean( false );
}
else{
out.writeBoolean( true );
PreferenceFactory factory = factories.get( type );
if( factory == null )
throw new IOException( "unknown path for a type: " + type );
factory.write( value, out );
}
}
int size = node.getChildrenCount();
out.writeInt( size );
for( int i = 0; i < size; i++ ){
Node child = node.getChild( i );
out.writeUTF( child.getName() );
write( child, out );
}
}
/**
* Reads the contents of this storage from a stream. Note that this method does
* not clear the storage, if there are values in this storage that are not
* in the stream, then these values remain.
* @param in the stream to read from
* @throws IOException if <code>in</code> can't be read
*/
public void read( DataInputStream in ) throws IOException{
Version version = Version.read( in );
version.checkCurrent();
read( root, in );
}
@SuppressWarnings("unchecked")
private void read( Node node, DataInputStream in ) throws IOException{
Path type = null;
Object value = null;
if( in.readBoolean() ){
type = new Path( in.readUTF() );
if( in.readBoolean() ){
PreferenceFactory factory = factories.get( type );
if( factory == null )
throw new IOException( "don't know how to read objects of type " + type );
value = factory.read( in );
}
}
node.put( type, value );
int size = in.readInt();
for( int i = 0; i < size; i++ ){
String name = in.readUTF();
Node child = node.getNode( new Path( name ), true );
read( child, in );
}
}
/**
* Writes the contents of this storage into <code>element</code>, adds
* new {@link XElement elements} to <code>element</code> but does
* not change the attributes. If a factory for some element is missing,
* then this element will not be stored.
* @param element the element to write into
*/
public void writeXML( XElement element ){
writeXML( root, element );
}
@SuppressWarnings("unchecked")
private void writeXML( Node node, XElement element ){
Path type = node.getType();
Object value = node.getValue();
if( type != null && value != null ){
PreferenceFactory factory = factories.get( type );
if( factory != null ){
XElement xvalue = element.addElement( "value" );
xvalue.addString( "type", type.toString() );
factory.writeXML( value, xvalue );
}
}
int size = node.getChildrenCount();
for( int i = 0; i < size; i++ ){
Node child = node.getChild( i );
XElement xchild = element.addElement( "child" );
xchild.addString( "name", child.getName() );
writeXML( child, xchild );
}
}
/**
* Reads the contents of this storage from <code>element</code>. Note that this
* method does not clear the storage, if there are values in this storage that are not
* in <code>element</code>, then these values remain. If there is a
* {@link PreferenceFactory} missing for some type, then this value will silently
* be left out.
* @param element the element to read from
* @throws XException if <code>element</code> is not correct
*/
public void readXML( XElement element ){
readXML( root, element );
}
@SuppressWarnings("unchecked")
private void readXML( Node node, XElement element ){
Path type = null;
Object value = null;
XElement xvalue = element.getElement( "value" );
if( xvalue != null ){
String typeName = xvalue.getString( "type" );
type = new Path( typeName );
PreferenceFactory factory = factories.get( type );
if( factory != null ){
value = factory.readXML( xvalue );
}
}
node.put( type, value );
XElement[] xchildren = element.getElements( "child" );
for( XElement xchild : xchildren ){
String name = xchild.getString( "name" );
Node child = node.getNode( new Path( name ), true );
readXML( child, xchild );
}
}
/**
* Removes all preferences from this storage
*/
public void clear(){
root = new Node( null );
}
/**
* Represents a single resource in a {@link PreferenceStorage}.
* @author Benjamin Sigg
*/
private static class Node{
/** the name of this node */
private String name;
/** the value of the preference stored in this node, may be <code>null</code> */
private Object value;
/**
* the kind of value stored in this node, may be <code>null</code> to indicate
* that this node does not represent a preference
*/
private Path type;
/** additional nodes that are nested into this node */
private List<Node> children;
/**
* Creates a new node
* @param name the name of this node
*/
public Node( String name ){
this.name = name;
}
/**
* Sets type and value of this node
* @param type new type
* @param value new value, should be <code>null</code> or a subclass
* of <code>type</code>
*/
public void put( Path type, Object value ){
this.type = type;
this.value = value;
}
/**
* Gets the type of this node, might be <code>null</code>
* @return the type
*/
public Path getType() {
return type;
}
/**
* Gets the value of this node, might be <code>null</code>. Is
* surely <code>null</code> if {@link #getType()} returns <code>null</code>
* @return the value
*/
public Object getValue() {
return value;
}
/**
* Gets the number of children of this node.
* @return the number of children
*/
public int getChildrenCount(){
return children == null ? 0 : children.size();
}
/**
* Gets the index'th child of this node
* @param index the location of the child
* @return the child
*/
public Node getChild( int index ){
return children.get( index );
}
/**
* Gets the name of this node.
* @return the name
*/
public String getName() {
return name;
}
/**
* Searches or creates a node for <code>path</code>.
* @param path the path of some node
* @param create whether to create the node if it does not exist
* @return the node at the end of <code>path</code>
*/
public Node getNode( Path path, boolean create ){
return getNode( path, create, 0 );
}
private Node getNode( Path path, boolean create, int segment ){
if( path.getSegmentCount() == segment )
return this;
if( children == null ){
children = new ArrayList<Node>();
}
String name = path.getSegment( segment );
for( Node child : children ){
if( name.equals( child.getName() )){
return child.getNode( path, create, segment+1 );
}
}
if( !create )
return null;
Node result = new Node( name );
children.add( result );
return result.getNode( path, create, segment+1 );
}
}
}