Browse Source

Add support for modifying properties

git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@816 f203690c-595d-4dc9-a70b-905162fa7fd2
tags/jackcess-2.0.1
James Ahlborn 10 years ago
parent
commit
bcbcda7563

+ 3
- 0
src/changes/changes.xml View 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">

+ 235
- 0
src/main/java/com/healthmarketscience/jackcess/impl/ByteArrayBuilder.java View File

@@ -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);
}
}
}

+ 31
- 25
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java View 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

+ 18
- 0
src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java View 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;

+ 12
- 20
src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java View 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();

+ 147
- 24
src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java View 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));
}


+ 55
- 5
src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java View 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();
}
}

}

Loading…
Cancel
Save