]> source.dussan.org Git - jackcess.git/commitdiff
Add support for modifying properties
authorJames Ahlborn <jtahlborn@yahoo.com>
Fri, 11 Oct 2013 01:43:24 +0000 (01:43 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Fri, 11 Oct 2013 01:43:24 +0000 (01:43 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@816 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/main/java/com/healthmarketscience/jackcess/impl/ByteArrayBuilder.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java
src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java
src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java

index b9d3e64fd26896df7b0db8f73420dd13db988e88..1aa8d52eb032d0f745d363def2ab37bca47d1790 100644 (file)
@@ -14,6 +14,9 @@
       <action dev="jahlborn" type="fix">
         Make reading long value columns more lenient (MEMO/OLE).
       </action>
+      <action dev="jahlborn" type="add">
+        Add support for modifying properties.
+      </action>
     </release>
     <release version="2.0.0" date="2013-08-26">
       <action dev="jahlborn" type="update">
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ByteArrayBuilder.java b/src/main/java/com/healthmarketscience/jackcess/impl/ByteArrayBuilder.java
new file mode 100644 (file)
index 0000000..ade3eed
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+Copyright (c) 2013 James Ahlborn
+
+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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.impl;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Utility class for constructing {@code byte[]s} where the final size of the
+ * data is not known beforehand.  The API is similar to {@code ByteBuffer} but
+ * the data is not actually written to a {@code byte[]} until {@link
+ * #toBuffer} or {@link #toArray} is called.
+ *
+ * @author James Ahlborn
+ */
+public class ByteArrayBuilder 
+{
+  private int _pos;
+  private final List<Data> _data = new ArrayList<Data>();
+
+  public ByteArrayBuilder() {
+  }
+
+  public int position() {
+    return _pos;
+  }
+
+  public ByteArrayBuilder reserveInt() {
+    return reserve(4);
+  }
+
+  public ByteArrayBuilder reserveShort() {
+    return reserve(2);
+  }
+
+  public ByteArrayBuilder reserve(int bytes) {
+    _pos += bytes;
+    return this;
+  }
+
+  public ByteArrayBuilder put(byte val) {
+    return put(new ByteData(_pos, val));
+  }
+
+  public ByteArrayBuilder putInt(int val) {
+    return putInt(_pos, val);
+  }
+
+  public ByteArrayBuilder putInt(int pos, int val) {
+    return put(new IntData(pos, val));
+  }
+
+  public ByteArrayBuilder putShort(short val) {
+    return putShort(_pos, val);
+  }
+
+  public ByteArrayBuilder putShort(int pos, short val) {
+    return put(new ShortData(pos, val));
+  }
+
+  public ByteArrayBuilder put(byte[] val) {
+    return put(new BytesData(_pos, val));
+  }
+
+  public ByteArrayBuilder put(ByteBuffer val) {
+    return put(new BufData(_pos, val));
+  }
+
+  private ByteArrayBuilder put(Data data) {
+    _data.add(data);
+    int endPos = data.getEndPos();
+    if(endPos > _pos) {
+      _pos = endPos;
+    }
+    return this;
+  }
+
+  public ByteBuffer toBuffer() {
+    return toBuffer(PageChannel.wrap(new byte[_pos]));
+  }
+
+  public ByteBuffer toBuffer(ByteBuffer buf) {
+    for(Data data : _data) {
+      data.write(buf);
+    }
+    buf.rewind();
+    return buf;
+  }
+
+  public byte[] toArray() {
+    return toBuffer().array();
+  }
+
+  private static abstract class Data
+  {
+    private int _pos;
+    
+    protected Data(int pos) {
+      _pos = pos;
+    }
+
+    public int getPos() {
+      return _pos;
+    }
+
+    public int getEndPos() {
+      return getPos() + size();
+    }
+
+    public abstract int size();
+
+    public abstract void write(ByteBuffer buf);
+  }
+
+  private static final class IntData extends Data
+  {
+    private final int _val;
+
+    private IntData(int pos, int val) {
+      super(pos);
+      _val = val;
+    }
+
+    @Override
+    public int size() {
+      return 4;
+    }
+
+    @Override
+    public void write(ByteBuffer buf) {
+      buf.putInt(getPos(), _val);
+    }
+  }
+
+  private static final class ShortData extends Data
+  {
+    private final short _val;
+
+    private ShortData(int pos, short val) {
+      super(pos);
+      _val = val;
+    }
+
+    @Override
+    public int size() {
+      return 2;
+    }
+
+    @Override
+    public void write(ByteBuffer buf) {
+      buf.putShort(getPos(), _val);
+    }
+  }
+
+  private static final class ByteData extends Data
+  {
+    private final byte _val;
+
+    private ByteData(int pos, byte val) {
+      super(pos);
+      _val = val;
+    }
+
+    @Override
+    public int size() {
+      return 1;
+    }
+
+    @Override
+    public void write(ByteBuffer buf) {
+      buf.put(getPos(), _val);
+    }
+  }
+
+  private static final class BytesData extends Data
+  {
+    private final byte[] _val;
+
+    private BytesData(int pos, byte[] val) {
+      super(pos);
+      _val = val;
+    }
+
+    @Override
+    public int size() {
+      return _val.length;
+    }
+
+    @Override
+    public void write(ByteBuffer buf) {
+      buf.position(getPos());
+      buf.put(_val);
+    }
+  }
+
+  private static final class BufData extends Data
+  {
+    private final ByteBuffer _val;
+
+    private BufData(int pos, ByteBuffer val) {
+      super(pos);
+      _val = val;
+    }
+
+    @Override
+    public int size() {
+      return _val.remaining();
+    }
+
+    @Override
+    public void write(ByteBuffer buf) {
+      buf.position(getPos());
+      buf.put(_val);
+    }
+  }
+}
index 65b276f0f13dc6113803168d0b6c47529c94eaaa..a6cba978f8deed169f0df8d2a1ce8ac93426593c 100644 (file)
@@ -1016,35 +1016,41 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
     throws IOException
   {
     Matcher m = GUID_PATTERN.matcher(toCharSequence(value));
-    if(m.matches()) {
-      ByteBuffer origBuffer = null;
-      byte[] tmpBuf = null;
-      if(order != ByteOrder.BIG_ENDIAN) {
-        // write to a temp buf so we can do some swapping below
-        origBuffer = buffer;
-        tmpBuf = new byte[16];
-        buffer = ByteBuffer.wrap(tmpBuf);
-      }
+    if(!m.matches()) {
+      throw new IOException("Invalid GUID: " + value);
+    }
 
-      ByteUtil.writeHexString(buffer, m.group(1));
-      ByteUtil.writeHexString(buffer, m.group(2));
-      ByteUtil.writeHexString(buffer, m.group(3));
-      ByteUtil.writeHexString(buffer, m.group(4));
-      ByteUtil.writeHexString(buffer, m.group(5));
-      
-      if(tmpBuf != null) {
-        // the first 3 guid components are integer components which need to
-        // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int
-        ByteUtil.swap4Bytes(tmpBuf, 0);
-        ByteUtil.swap2Bytes(tmpBuf, 4);
-        ByteUtil.swap2Bytes(tmpBuf, 6);
-        origBuffer.put(tmpBuf);
-      }
+    ByteBuffer origBuffer = null;
+    byte[] tmpBuf = null;
+    if(order != ByteOrder.BIG_ENDIAN) {
+      // write to a temp buf so we can do some swapping below
+      origBuffer = buffer;
+      tmpBuf = new byte[16];
+      buffer = ByteBuffer.wrap(tmpBuf);
+    }
 
-    } else {
-      throw new IOException("Invalid GUID: " + value);
+    ByteUtil.writeHexString(buffer, m.group(1));
+    ByteUtil.writeHexString(buffer, m.group(2));
+    ByteUtil.writeHexString(buffer, m.group(3));
+    ByteUtil.writeHexString(buffer, m.group(4));
+    ByteUtil.writeHexString(buffer, m.group(5));
+      
+    if(tmpBuf != null) {
+      // the first 3 guid components are integer components which need to
+      // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int
+      ByteUtil.swap4Bytes(tmpBuf, 0);
+      ByteUtil.swap2Bytes(tmpBuf, 4);
+      ByteUtil.swap2Bytes(tmpBuf, 6);
+      origBuffer.put(tmpBuf);
     }
   }
+
+  /**
+   * Returns {@code true} if the given value is a "guid" value.
+   */
+  static boolean isGUIDValue(Object value) throws IOException {
+    return GUID_PATTERN.matcher(toCharSequence(value)).matches();
+  }
   
   /**
    * Write an LVAL column into a ByteBuffer inline if it fits, otherwise in
index 70f5fd92fe10bd6dc0d99f4086868f170d9e5c9c..ad5eb28798e536f1f66bc2ccf23380b0ab7e9b6d 100644 (file)
@@ -263,6 +263,7 @@ public abstract class JetFormat {
   
   public final Charset CHARSET;
   public final ColumnImpl.SortOrder DEFAULT_SORT_ORDER;
+  public final byte[] PROPERTY_MAP_TYPE;
   
   /**
    * @param channel the database file.
@@ -397,6 +398,7 @@ public abstract class JetFormat {
     
     CHARSET = defineCharset();
     DEFAULT_SORT_ORDER = defineDefaultSortOrder();
+    PROPERTY_MAP_TYPE = definePropMapType();
   }
   
   protected abstract boolean defineReadOnly();
@@ -497,6 +499,7 @@ public abstract class JetFormat {
   
   protected abstract Charset defineCharset();
   protected abstract ColumnImpl.SortOrder defineDefaultSortOrder();
+  protected abstract byte[] definePropMapType();
 
   protected abstract boolean defineLegacyNumericIndexes();
 
@@ -715,6 +718,11 @@ public abstract class JetFormat {
       return ColumnImpl.GENERAL_LEGACY_SORT_ORDER;
     }
 
+    @Override
+    protected byte[] definePropMapType() {
+      return PROPERTY_MAP_TYPES[1];
+    }
+    
     @Override
     protected Map<String,Database.FileFormat> getPossibleFileFormats()
     {
@@ -935,6 +943,11 @@ public abstract class JetFormat {
       return ColumnImpl.GENERAL_LEGACY_SORT_ORDER;
     }
 
+    @Override
+    protected byte[] definePropMapType() {
+      return PROPERTY_MAP_TYPES[0];
+    }
+
     @Override
     protected Map<String,Database.FileFormat> getPossibleFileFormats()
     {
@@ -1009,6 +1022,11 @@ public abstract class JetFormat {
       return ColumnImpl.GENERAL_SORT_ORDER;
     }
 
+    @Override
+    protected byte[] definePropMapType() {
+      return PROPERTY_MAP_TYPES[0];
+    }
+
     @Override
     protected Map<String,Database.FileFormat> getPossibleFileFormats() {
       return PossibleFileFormats.POSSIBLE_VERSION_14;
index 7b2b91991b9c8d299f40a36147122b7fb59497b4..c084583475852f8d4449686349c7586be9e64227 100644 (file)
@@ -37,10 +37,12 @@ public class PropertyMapImpl implements PropertyMap
   private final short _mapType;
   private final Map<String,Property> _props = 
     new LinkedHashMap<String,Property>();
+  private final PropertyMaps _owner;
 
-  public PropertyMapImpl(String name, short type) {
+  public PropertyMapImpl(String name, short type, PropertyMaps owner) {
     _mapName = name;
     _mapType = type;
+    _owner = owner;
   }
 
   public String getName() {
@@ -51,6 +53,10 @@ public class PropertyMapImpl implements PropertyMap
     return _mapType;
   }
 
+  public PropertyMaps getOwner() {
+    return _owner;
+  }
+
   public int getSize() {
     return _props.size();
   }
@@ -88,24 +94,6 @@ public class PropertyMapImpl implements PropertyMap
     return _props.values().iterator();
   }
 
-  public PropertyMapImpl merge(PropertyMapImpl opm) {
-    if(opm == null) {
-      return this;          
-    }
-
-    // merge into least map type
-    PropertyMapImpl dest = opm;
-    PropertyMapImpl src = this;
-    if(dest._mapType < src._mapType) {
-      dest = this;
-      src = opm;
-    }
-
-    dest._props.putAll(src._props);
-
-    return dest;
-  }
-
 
   @Override
   public String toString() {
@@ -126,7 +114,7 @@ public class PropertyMapImpl implements PropertyMap
   /**
    * Info about a property defined in a PropertyMap.
    */ 
-  private static final class PropertyImpl implements PropertyMap.Property
+  static final class PropertyImpl implements PropertyMap.Property
   {
     private final String _name;
     private final DataType _type;
@@ -152,6 +140,10 @@ public class PropertyMapImpl implements PropertyMap
       return _value;
     }
 
+    public byte getFlag() {
+      return _flag;
+    }
+
     @Override
     public String toString() {
       Object val = getValue();
index b0d55672f15647fba1c62b55528009fa3da10747..b3e345b2e86a788342f8c8ff26268baf6075f390 100644 (file)
@@ -25,11 +25,13 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
-import com.healthmarketscience.jackcess.PropertyMap;
 import com.healthmarketscience.jackcess.DataType;
+import com.healthmarketscience.jackcess.PropertyMap;
 
 /**
  * Collection of PropertyMap instances read from a single property data block.
@@ -50,9 +52,11 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
   private final Map<String,PropertyMapImpl> _maps = 
     new LinkedHashMap<String,PropertyMapImpl>();
   private final int _objectId;
+  private final Handler _handler;
 
-  public PropertyMaps(int objectId) {
+  public PropertyMaps(int objectId, Handler handler) {
     _objectId = objectId;
+    _handler = handler;
   }
 
   public int getObjectId() {
@@ -91,24 +95,20 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
     String lookupName = DatabaseImpl.toLookupName(name);
     PropertyMapImpl map = _maps.get(lookupName);
     if(map == null) {
-      map = new PropertyMapImpl(name, type);
+      map = new PropertyMapImpl(name, type, this);
       _maps.put(lookupName, map);
     }
     return map;
   }
 
-  /**
-   * Adds the given PropertyMap to this group.
-   */
-  public void put(PropertyMapImpl map) {
-    String mapName = DatabaseImpl.toLookupName(map.getName());
-    _maps.put(mapName, map.merge(_maps.get(mapName)));
-  }
-
   public Iterator<PropertyMapImpl> iterator() {
     return _maps.values().iterator();
   }
 
+  public byte[] write() throws IOException {
+    return _handler.write(this);
+  }
+
   @Override
   public String toString() {
     return CustomToStringStyle.builder(this)
@@ -138,14 +138,12 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
     public PropertyMaps read(byte[] propBytes, int objectId) 
       throws IOException 
     {
-
-      PropertyMaps maps = new PropertyMaps(objectId);
+      PropertyMaps maps = new PropertyMaps(objectId, this);
       if((propBytes == null) || (propBytes.length == 0)) {
         return maps;
       }
 
-      ByteBuffer bb = ByteBuffer.wrap(propBytes)
-        .order(PageChannel.DEFAULT_BYTE_ORDER);
+      ByteBuffer bb = PageChannel.wrap(propBytes);
 
       // check for known header
       boolean knownType = false;
@@ -176,7 +174,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
         if(type == PROPERTY_NAME_LIST) {
           propNames = readPropertyNames(bbBlock);
         } else {
-          maps.put(readPropertyValues(bbBlock, propNames, type));
+          readPropertyValues(bbBlock, propNames, type, maps);
         }
 
         bb.position(endPos);
@@ -185,6 +183,58 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
       return maps;
     }
 
+    /**
+     * @return a byte[] encoded from the given PropertyMaps instance
+     */
+    public byte[] write(PropertyMaps maps)
+      throws IOException
+    {
+      if(maps == null) {
+        return null;
+      }
+
+      ByteArrayBuilder bab = new ByteArrayBuilder();
+
+      bab.put(_database.getFormat().PROPERTY_MAP_TYPE);
+
+      // grab the property names from all the maps
+      Set<String> propNames = new LinkedHashSet<String>();
+      for(PropertyMapImpl propMap : maps) {
+        for(PropertyMap.Property prop : propMap) {
+          propNames.add(prop.getName());
+        }
+      }
+
+      // write the full set of property names
+      writeBlock(null, propNames, PROPERTY_NAME_LIST, bab);
+
+      // write all the map values
+      for(PropertyMapImpl propMap : maps) {
+        writeBlock(propMap, propNames, propMap.getType(), bab);
+      }
+      
+      return bab.toArray();
+    }
+
+    private void writeBlock(
+        PropertyMapImpl propMap, Set<String> propNames,
+        short blockType, ByteArrayBuilder bab)
+      throws IOException
+    {
+      int blockStartPos = bab.position();
+      bab.reserveInt()
+        .putShort(blockType);
+
+      if(blockType == PROPERTY_NAME_LIST) {
+        writePropertyNames(propNames, bab);
+      } else {
+        writePropertyValues(propMap, propNames, bab);
+      }      
+
+      int len = bab.position() - blockStartPos;
+      bab.putInt(blockStartPos, len);
+    }
+    
     /**
      * @return the property names parsed from the given data chunk
      */
@@ -196,12 +246,20 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
       return names;
     }
 
+    private void writePropertyNames(Set<String> propNames,
+                                    ByteArrayBuilder bab) {
+      for(String propName : propNames) {
+        writePropName(propName, bab);
+      }      
+    }
+
     /**
      * @return the PropertyMap created from the values parsed from the given
      *         data chunk combined with the given property names
      */
     private PropertyMapImpl readPropertyValues(
-        ByteBuffer bbBlock, List<String> propNames, short blockType) 
+        ByteBuffer bbBlock, List<String> propNames, short blockType,
+        PropertyMaps maps) 
       throws IOException
     {
       String mapName = DEFAULT_NAME;
@@ -217,12 +275,12 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
         bbBlock.position(endPos);
       }
       
-      PropertyMapImpl map = new PropertyMapImpl(mapName, blockType);
+      PropertyMapImpl map = maps.get(mapName, blockType);
 
       // read the values
       while(bbBlock.hasRemaining()) {
 
-        int valLen = bbBlock.getShort();
+        int valLen = bbBlock.getShort();        
         int endPos = bbBlock.position() + valLen - 2;
         byte flag = bbBlock.get();
         DataType dataType = DataType.fromByte(bbBlock.get());
@@ -230,7 +288,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
         int dataSize = bbBlock.getShort();
 
         String propName = propNames.get(nameIdx);
-        PropColumn col = getColumn(dataType, propName, dataSize);
+        PropColumn col = getColumn(dataType, propName, dataSize, null);
 
         byte[] data = ByteUtil.getBytes(bbBlock, dataSize);
         Object value = col.read(data);
@@ -243,6 +301,54 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
       return map;
     }
 
+    private void writePropertyValues(
+        PropertyMapImpl propMap, Set<String> propNames, ByteArrayBuilder bab) 
+      throws IOException
+    {      
+      // write the map name, if any
+      String mapName = propMap.getName();
+      int blockStartPos = bab.position();
+      bab.reserveInt();
+      writePropName(mapName, bab);
+      int len = bab.position() - blockStartPos;
+      bab.putInt(blockStartPos, len);
+
+      // write the map values
+      int nameIdx = 0;
+      for(String propName : propNames) {
+
+        PropertyMapImpl.PropertyImpl prop = (PropertyMapImpl.PropertyImpl)
+          propMap.get(propName);
+
+        if(prop != null) {
+
+          Object value = prop.getValue();
+          if(value != null) {
+
+            int valStartPos = bab.position();
+            bab.reserveShort();
+
+            bab.put(prop.getFlag());
+            bab.put(prop.getType().getValue());
+            bab.putShort((short)nameIdx);
+
+            PropColumn col = getColumn(prop.getType(), propName, -1, value);
+
+            ByteBuffer data = col.write(
+                value, _database.getFormat().MAX_ROW_SIZE);
+
+            bab.putShort((short)data.remaining());
+            bab.put(data);
+
+            len = bab.position() - valStartPos;
+            bab.putShort(valStartPos, (short)len);
+          }
+        }
+
+        ++nameIdx;
+      }
+    }
+
     /**
      * Reads a property name from the given data block
      */
@@ -252,14 +358,26 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
       return ColumnImpl.decodeUncompressedText(nameBytes, _database.getCharset());
     }
 
+    /**
+     * Writes a property name to the given data block
+     */
+    private void writePropName(String propName, ByteArrayBuilder bab) {
+      ByteBuffer textBuf = ColumnImpl.encodeUncompressedText(
+          propName, _database.getCharset());
+      bab.putShort((short)textBuf.remaining());
+      bab.put(textBuf);
+    }
+
     /**
      * Gets a PropColumn capable of reading/writing a property of the given
      * DataType
      */
     private PropColumn getColumn(DataType dataType, String propName, 
-                                 int dataSize) {
+                                 int dataSize, Object value) 
+      throws IOException
+    {
 
-      if(isPseudoGuidColumn(dataType, propName, dataSize)) {
+      if(isPseudoGuidColumn(dataType, propName, dataSize, value)) {
         dataType = DataType.GUID;
       }
 
@@ -278,16 +396,21 @@ public class PropertyMaps implements Iterable<PropertyMapImpl>
         // create column with ability to read/write the given data type
         col = ((colType == DataType.BOOLEAN) ? 
                new BooleanPropColumn() : new PropColumn(colType));
+
+        _columns.put(dataType, col);
       }
 
       return col;
     }
 
     private static boolean isPseudoGuidColumn(
-        DataType dataType, String propName, int dataSize) {
+        DataType dataType, String propName, int dataSize, Object value) 
+      throws IOException
+    {
       // guids seem to be marked as "binary" fields
       return((dataType == DataType.BINARY) && 
-             (dataSize == DataType.GUID.getFixedSize()) &&
+             ((dataSize == DataType.GUID.getFixedSize()) ||
+              ((dataSize == -1) && ColumnImpl.isGUIDValue(value))) &&
              PropertyMap.GUID_PROP.equalsIgnoreCase(propName));
     }
 
index 8cd5a554fbd82c894a173c857bc97ff1e39272d8..2670ae7f7573a5ca43bf4b7b6fe804eec3c39abe 100644 (file)
@@ -22,17 +22,19 @@ package com.healthmarketscience.jackcess;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
-import junit.framework.TestCase;
-import com.healthmarketscience.jackcess.impl.PropertyMapImpl;
-import com.healthmarketscience.jackcess.impl.PropertyMaps;
 import static com.healthmarketscience.jackcess.Database.*;
 import static com.healthmarketscience.jackcess.DatabaseTest.*;
+import com.healthmarketscience.jackcess.impl.ByteUtil;
+import com.healthmarketscience.jackcess.impl.DatabaseImpl;
 import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
+import com.healthmarketscience.jackcess.impl.PropertyMapImpl;
+import com.healthmarketscience.jackcess.impl.PropertyMaps;
 import com.healthmarketscience.jackcess.impl.TableImpl;
-import com.healthmarketscience.jackcess.impl.DatabaseImpl;
+import junit.framework.TestCase;
 
 /**
  * @author James Ahlborn
@@ -46,7 +48,7 @@ public class PropertiesTest extends TestCase
 
   public void testPropertyMaps() throws Exception
   {
-    PropertyMaps maps = new PropertyMaps(10);
+    PropertyMaps maps = new PropertyMaps(10, null);
     assertTrue(maps.isEmpty());
     assertEquals(0, maps.getSize());
     assertFalse(maps.iterator().hasNext());
@@ -208,4 +210,52 @@ public class PropertiesTest extends TestCase
     }
   }
 
+  public void testWriteProperties() throws Exception
+  {
+    for(TestDB testDb : SUPPORTED_DBS_TEST) {
+      Database db = open(testDb);
+
+      TableImpl t = (TableImpl)db.getTable("Table1");
+
+      PropertyMap tProps = t.getProperties();
+
+      PropertyMaps maps = ((PropertyMapImpl)tProps).getOwner();
+
+      byte[] mapsBytes = maps.write();
+
+      PropertyMaps maps2 = ((DatabaseImpl)db).readProperties(
+          mapsBytes, maps.getObjectId());
+
+      Iterator<PropertyMapImpl> iter = maps.iterator();
+      Iterator<PropertyMapImpl> iter2 = maps2.iterator();
+
+      while(iter.hasNext() && iter2.hasNext()) {
+        PropertyMapImpl propMap = iter.next();
+        PropertyMapImpl propMap2 = iter2.next();
+
+        assertEquals(propMap.getSize(), propMap2.getSize());
+        for(PropertyMap.Property prop : propMap) {
+          PropertyMap.Property prop2 = propMap2.get(prop.getName());
+
+          assertEquals(prop.getName(), prop2.getName());
+          assertEquals(prop.getType(), prop2.getType());
+
+          Object v1 = prop.getValue();
+          Object v2 = prop2.getValue();
+
+          if(v1 instanceof byte[]) {
+            assertTrue(Arrays.equals((byte[])v1, (byte[])v2));
+          } else {
+            assertEquals(v1, v2);
+          }
+        }
+      }
+      
+      assertFalse(iter.hasNext());
+      assertFalse(iter2.hasNext());
+
+      db.close();
+    }    
+  }
+
 }