<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">
--- /dev/null
+/*
+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);
+ }
+ }
+}
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
public final Charset CHARSET;
public final ColumnImpl.SortOrder DEFAULT_SORT_ORDER;
+ public final byte[] PROPERTY_MAP_TYPE;
/**
* @param channel the database file.
CHARSET = defineCharset();
DEFAULT_SORT_ORDER = defineDefaultSortOrder();
+ PROPERTY_MAP_TYPE = definePropMapType();
}
protected abstract boolean defineReadOnly();
protected abstract Charset defineCharset();
protected abstract ColumnImpl.SortOrder defineDefaultSortOrder();
+ protected abstract byte[] definePropMapType();
protected abstract boolean defineLegacyNumericIndexes();
return ColumnImpl.GENERAL_LEGACY_SORT_ORDER;
}
+ @Override
+ protected byte[] definePropMapType() {
+ return PROPERTY_MAP_TYPES[1];
+ }
+
@Override
protected Map<String,Database.FileFormat> getPossibleFileFormats()
{
return ColumnImpl.GENERAL_LEGACY_SORT_ORDER;
}
+ @Override
+ protected byte[] definePropMapType() {
+ return PROPERTY_MAP_TYPES[0];
+ }
+
@Override
protected Map<String,Database.FileFormat> getPossibleFileFormats()
{
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;
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() {
return _mapType;
}
+ public PropertyMaps getOwner() {
+ return _owner;
+ }
+
public int getSize() {
return _props.size();
}
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() {
/**
* 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;
return _value;
}
+ public byte getFlag() {
+ return _flag;
+ }
+
@Override
public String toString() {
Object val = getValue();
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.
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() {
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)
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;
if(type == PROPERTY_NAME_LIST) {
propNames = readPropertyNames(bbBlock);
} else {
- maps.put(readPropertyValues(bbBlock, propNames, type));
+ readPropertyValues(bbBlock, propNames, type, maps);
}
bb.position(endPos);
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
*/
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;
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());
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);
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
*/
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;
}
// 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));
}
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
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());
}
}
+ 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();
+ }
+ }
+
}