From bcbcda7563428dff2820ec142e05a31f342eb293 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Fri, 11 Oct 2013 01:43:24 +0000 Subject: [PATCH] Add support for modifying properties git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@816 f203690c-595d-4dc9-a70b-905162fa7fd2 --- src/changes/changes.xml | 3 + .../jackcess/impl/ByteArrayBuilder.java | 235 ++++++++++++++++++ .../jackcess/impl/ColumnImpl.java | 56 +++-- .../jackcess/impl/JetFormat.java | 18 ++ .../jackcess/impl/PropertyMapImpl.java | 32 +-- .../jackcess/impl/PropertyMaps.java | 171 +++++++++++-- .../jackcess/PropertiesTest.java | 60 ++++- 7 files changed, 501 insertions(+), 74 deletions(-) create mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/ByteArrayBuilder.java diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b9d3e64..1aa8d52 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -14,6 +14,9 @@ Make reading long value columns more lenient (MEMO/OLE). + + Add support for modifying properties. + 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 index 0000000..ade3eed --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ByteArrayBuilder.java @@ -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 = new ArrayList(); + + 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); + } + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 65b276f..a6cba97 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -1016,35 +1016,41 @@ public class ColumnImpl implements Column, Comparable { 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 diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java b/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java index 70f5fd9..ad5eb28 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java @@ -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 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 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 getPossibleFileFormats() { return PossibleFileFormats.POSSIBLE_VERSION_14; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java index 7b2b919..c084583 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java @@ -37,10 +37,12 @@ public class PropertyMapImpl implements PropertyMap private final short _mapType; private final Map _props = new LinkedHashMap(); + 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(); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java index b0d5567..b3e345b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java @@ -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 private final Map _maps = new LinkedHashMap(); 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 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 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 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 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 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 propNames = new LinkedHashSet(); + 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 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 return names; } + private void writePropertyNames(Set 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 propNames, short blockType) + ByteBuffer bbBlock, List propNames, short blockType, + PropertyMaps maps) throws IOException { String mapName = DEFAULT_NAME; @@ -217,12 +275,12 @@ public class PropertyMaps implements Iterable 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 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 return map; } + private void writePropertyValues( + PropertyMapImpl propMap, Set 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 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 // 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)); } diff --git a/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java b/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java index 8cd5a55..2670ae7 100644 --- a/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java @@ -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 iter = maps.iterator(); + Iterator 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(); + } + } + } -- 2.39.5