From: James Ahlborn Date: Tue, 27 Oct 2009 01:26:46 +0000 (+0000) Subject: initial support for guid indexes and guid autonumbers X-Git-Tag: jackcess-1.1.20~11 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=03de09743b2d808221dcc30795d329d45a6fae21;p=jackcess.git initial support for guid indexes and guid autonumbers git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@406 f203690c-595d-4dc9-a70b-905162fa7fd2 --- diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 8416262..2be53cf 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -4,6 +4,12 @@ Tim McCune + + + Add support for updating GUID indexes and for auto-number GUID + fields. + + Add Query reading support. diff --git a/src/java/com/healthmarketscience/jackcess/ByteUtil.java b/src/java/com/healthmarketscience/jackcess/ByteUtil.java index 0ea90ce..95c2d8e 100644 --- a/src/java/com/healthmarketscience/jackcess/ByteUtil.java +++ b/src/java/com/healthmarketscience/jackcess/ByteUtil.java @@ -242,7 +242,7 @@ public final class ByteUtil { * @param order the order to insert the bytes of the int */ public static void putInt(ByteBuffer buffer, int val, int offset, - ByteOrder order) + ByteOrder order) { ByteOrder origOrder = buffer.order(); try { @@ -416,5 +416,34 @@ public final class ByteUtil { public static int asUnsignedShort(short s) { return s & 0xFFFF; } + + /** + * Swaps the 4 bytes (changes endianness) of the bytes at the given offset. + * + * @param bytes buffer containing bytes to swap + * @param offset offset of the first byte of the bytes to swap + */ + public static void swap4Bytes(byte[] bytes, int offset) + { + byte b = bytes[offset + 0]; + bytes[offset + 0] = bytes[offset + 3]; + bytes[offset + 3] = b; + b = bytes[offset + 1]; + bytes[offset + 1] = bytes[offset + 2]; + bytes[offset + 2] = b; + } + + /** + * Swaps the 2 bytes (changes endianness) of the bytes at the given offset. + * + * @param bytes buffer containing bytes to swap + * @param offset offset of the first byte of the bytes to swap + */ + public static void swap2Bytes(byte[] bytes, int offset) + { + byte b = bytes[offset + 0]; + bytes[offset + 0] = bytes[offset + 1]; + bytes[offset + 1] = b; + } } diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index ea5b9c9..9f689b3 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -38,6 +38,7 @@ import java.sql.SQLException; import java.util.Calendar; import java.util.Date; import java.util.List; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -95,6 +96,9 @@ public class Column implements Comparable { /** mask for the auto number bit */ public static final byte AUTO_NUMBER_FLAG_MASK = (byte)0x04; + /** mask for the auto number guid bit */ + public static final byte AUTO_NUMBER_GUID_FLAG_MASK = (byte)0x40; + /** mask for the unknown bit */ public static final byte UNKNOWN_FLAG_MASK = (byte)0x02; @@ -132,6 +136,8 @@ public class Column implements Comparable { private int _fixedDataOffset; /** the index of the variable length data in the var len offset table */ private int _varLenTableIndex; + /** the auto number generator for this column (if autonumber column) */ + private AutoNumberGenerator _autoNumberGenerator; public Column() { this(JetFormat.VERSION_4); @@ -173,7 +179,9 @@ public class Column implements Comparable { } byte flags = buffer.get(offset + getFormat().OFFSET_COLUMN_FLAGS); _variableLength = ((flags & FIXED_LEN_FLAG_MASK) == 0); - _autoNumber = ((flags & AUTO_NUMBER_FLAG_MASK) != 0); + _autoNumber = ((flags & (AUTO_NUMBER_FLAG_MASK | AUTO_NUMBER_GUID_FLAG_MASK)) != 0); + setAutoNumberGenerator(); + _compressedUnicode = ((buffer.get(offset + getFormat().OFFSET_COLUMN_COMPRESSED_UNICODE) & 1) == 1); @@ -217,6 +225,7 @@ public class Column implements Comparable { public void setAutoNumber(boolean autoNumber) { _autoNumber = autoNumber; + setAutoNumberGenerator(); } public short getColumnNumber() { @@ -323,6 +332,29 @@ public class Column implements Comparable { return _fixedDataOffset; } + private void setAutoNumberGenerator() + { + if(!_autoNumber || (_type == null)) { + _autoNumberGenerator = null; + return; + } + + switch(_type) { + case LONG: + _autoNumberGenerator = new LongAutoNumberGenerator(); + break; + case GUID: + _autoNumberGenerator = new GuidAutoNumberGenerator(); + break; + default: + throw new RuntimeException("Unexpected autoNumber column type " + _type); + } + } + + public AutoNumberGenerator getAutoNumberGenerator() { + return _autoNumberGenerator; + } + /** * Checks that this column definition is valid. * @@ -368,9 +400,9 @@ public class Column implements Comparable { } if(isAutoNumber()) { - if(getType() != DataType.LONG) { + if((getType() != DataType.LONG) && (getType() != DataType.GUID)) { throw new IllegalArgumentException( - "Auto number column must be long integer"); + "Auto number column must be long integer or guid"); } } @@ -434,7 +466,7 @@ public class Column implements Comparable { } else if (_type == DataType.NUMERIC) { return readNumericValue(buffer); } else if (_type == DataType.GUID) { - return readGUIDValue(buffer); + return readGUIDValue(buffer, order); } else if ((_type == DataType.UNKNOWN_0D) || (_type == DataType.UNKNOWN_11)) { // treat like "binary" data @@ -717,8 +749,20 @@ public class Column implements Comparable { /** * Decodes a GUID value. */ - private String readGUIDValue(ByteBuffer buffer) + private String readGUIDValue(ByteBuffer buffer, ByteOrder order) { + if(order != ByteOrder.BIG_ENDIAN) { + byte[] tmpArr = new byte[16]; + buffer.get(tmpArr); + + // 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(tmpArr, 0); + ByteUtil.swap2Bytes(tmpArr, 4); + ByteUtil.swap2Bytes(tmpArr, 6); + buffer = ByteBuffer.wrap(tmpArr); + } + StringBuilder sb = new StringBuilder(22); sb.append("{"); sb.append(ByteUtil.toHexString(buffer, 0, 4, @@ -742,16 +786,36 @@ public class Column implements Comparable { /** * Writes a GUID value. */ - private void writeGUIDValue(ByteBuffer buffer, Object value) + private void writeGUIDValue(ByteBuffer buffer, Object value, + ByteOrder order) 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); + } + 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); + } + } else { throw new IOException("Invalid GUID: " + value); } @@ -1054,7 +1118,7 @@ public class Column implements Comparable { writeCurrencyValue(buffer, obj); break; case GUID: - writeGUIDValue(buffer, obj); + writeGUIDValue(buffer, obj, order); break; case NUMERIC: // yes, that's right, occasionally numeric values are written as fixed @@ -1225,7 +1289,7 @@ public class Column implements Comparable { rtn.append("\n\tCompressed Unicode: " + _compressedUnicode); } if(_autoNumber) { - rtn.append("\n\tNext AutoNumber: " + (_table.getLastAutoNumber() + 1)); + rtn.append("\n\tLast AutoNumber: " + _autoNumberGenerator.getLast()); } rtn.append("\n\n"); return rtn.toString(); @@ -1355,13 +1419,7 @@ public class Column implements Comparable { { // fix endianness of each 4 byte segment for(int i = 0; i < 4; ++i) { - int idx = i * 4; - byte b = bytes[idx + 0]; - bytes[idx + 0] = bytes[idx + 3]; - bytes[idx + 3] = b; - b = bytes[idx + 1]; - bytes[idx + 1] = bytes[idx + 2]; - bytes[idx + 2] = b; + ByteUtil.swap4Bytes(bytes, i * 4); } } @@ -1401,5 +1459,65 @@ public class Column implements Comparable { return new Date(super.getTime()); } } - + + /** + * Base class for the supported autonumber types. + */ + public abstract class AutoNumberGenerator + { + protected AutoNumberGenerator() {} + + public abstract Object getLast(); + + public abstract Object getNext(); + + public abstract int getColumnFlags(); + } + + private final class LongAutoNumberGenerator extends AutoNumberGenerator + { + private LongAutoNumberGenerator() {} + + @Override + public Object getLast() { + // the table stores the last long autonumber used + return getTable().getLastLongAutoNumber(); + } + + @Override + public Object getNext() { + // the table stores the last long autonumber used + return getTable().getNextLongAutoNumber(); + } + + @Override + public int getColumnFlags() { + return AUTO_NUMBER_FLAG_MASK; + } + } + + private final class GuidAutoNumberGenerator extends AutoNumberGenerator + { + private Object _lastAutoNumber; + + private GuidAutoNumberGenerator() {} + + @Override + public Object getLast() { + return _lastAutoNumber; + } + + @Override + public Object getNext() { + // format guids consistently w/ Column.readGUIDValue() + _lastAutoNumber = "{" + UUID.randomUUID() + "}"; + return _lastAutoNumber; + } + + @Override + public int getColumnFlags() { + return AUTO_NUMBER_GUID_FLAG_MASK; + } + } + } diff --git a/src/java/com/healthmarketscience/jackcess/DataType.java b/src/java/com/healthmarketscience/jackcess/DataType.java index b2e2178..0537103 100644 --- a/src/java/com/healthmarketscience/jackcess/DataType.java +++ b/src/java/com/healthmarketscience/jackcess/DataType.java @@ -127,7 +127,8 @@ public enum DataType { UNKNOWN_0D((byte) 0x0D, null, null, true, false, 0, 255, 255, 1), /** * Corresponds to a java String with the pattern - * "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}". Accepts any + * "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}", also known as a + * "Replication ID" in Access. Accepts any * Object converted to a String matching this pattern (surrounding "{}" are * optional, so {@link java.util.UUID}s are supported), or {@code null}. */ diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index 71380ce..d0aa45b 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -46,6 +46,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Date; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -561,9 +562,16 @@ public class Database } } - if(Table.countAutoNumberColumns(columns) > 1) { - throw new IllegalArgumentException( - "Can have at most one AutoNumber column per table"); + List autoCols = Table.getAutoNumberColumns(columns); + if(autoCols.size() > 1) { + // we can have one of each type + Set autoTypes = EnumSet.noneOf(DataType.class); + for(Column c : autoCols) { + if(!autoTypes.add(c.getType())) { + throw new IllegalArgumentException( + "Can have at most one AutoNumber column of type " + c.getType() + " per table"); + } + } } //Write the tdef page to disk. diff --git a/src/java/com/healthmarketscience/jackcess/Index.java b/src/java/com/healthmarketscience/jackcess/Index.java index 5363894..97315d6 100644 --- a/src/java/com/healthmarketscience/jackcess/Index.java +++ b/src/java/com/healthmarketscience/jackcess/Index.java @@ -1186,6 +1186,8 @@ public abstract class Index implements Comparable { return new ByteColumnDescriptor(col, flags); case BOOLEAN: return new BooleanColumnDescriptor(col, flags); + case GUID: + return new GuidColumnDescriptor(col, flags); default: // FIXME we can't modify this index at this point in time @@ -1480,6 +1482,41 @@ public abstract class Index implements Comparable { } } + /** + * ColumnDescriptor for guid columns. + */ + private static final class GuidColumnDescriptor extends ColumnDescriptor + { + private GuidColumnDescriptor(Column column, byte flags) + throws IOException + { + super(column, flags); + } + + @Override + protected void writeNonNullValue( + Object value, ByteArrayOutputStream bout) + throws IOException + { + byte[] valueBytes = encodeNumberColumnValue(value, getColumn()); + + // index format <8-bytes> 0x09 <8-bytes> 0x08 + + // bit twiddling rules: + // - isAsc => nothing + // - !isAsc => flipBytes, _but keep 09 unflipped_! + if(!isAscending()) { + flipBytes(valueBytes); + } + + bout.write(valueBytes, 0, 8); + bout.write(MID_GUID); + bout.write(valueBytes, 8, 8); + bout.write(isAscending() ? ASC_END_GUID : DESC_END_GUID); + } + } + + /** * ColumnDescriptor for columns which we cannot currently write. */ diff --git a/src/java/com/healthmarketscience/jackcess/IndexCodes.java b/src/java/com/healthmarketscience/jackcess/IndexCodes.java index a3c254d..88aa37c 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexCodes.java +++ b/src/java/com/healthmarketscience/jackcess/IndexCodes.java @@ -43,9 +43,12 @@ public class IndexCodes { static final byte DESC_NULL_FLAG = (byte)0xFF; static final byte END_TEXT = (byte)0x01; - static final byte END_EXTRA_TEXT = (byte)0x00; + static final byte MID_GUID = (byte)0x09; + static final byte ASC_END_GUID = (byte)0x08; + static final byte DESC_END_GUID = (byte)0xF7; + static final byte ASC_BOOLEAN_TRUE = (byte)0x00; static final byte ASC_BOOLEAN_FALSE = (byte)0xFF; diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index ecd3edf..585a619 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -87,8 +87,8 @@ public class Table private int _indexSlotCount; /** Number of rows in the table */ private int _rowCount; - /** last auto number for the table */ - private int _lastAutoNumber; + /** last long auto number for the table */ + private int _lastLongAutoNumber; /** page number of the definition of this table */ private final int _tableDefPageNumber; /** max Number of columns in the table (includes previous deletions) */ @@ -794,11 +794,7 @@ public class Table buffer.putShort((short) 0); //Unknown buffer.putInt(0); //Number of rows buffer.putInt(0); //Last Autonumber - if(countAutoNumberColumns(columns) > 0) { - buffer.put((byte) 1); - } else { - buffer.put((byte) 0); - } + buffer.put((byte) 1); // this makes autonumbering work in access for (int i = 0; i < 15; i++) { //Unknown buffer.put((byte) 0); } @@ -928,7 +924,7 @@ public class Table flags |= Column.FIXED_LEN_FLAG_MASK; } if(col.isAutoNumber()) { - flags |= Column.AUTO_NUMBER_FLAG_MASK; + flags |= col.getAutoNumberGenerator().getColumnFlags(); } return flags; } @@ -982,7 +978,7 @@ public class Table getFormat().SIZE_TDEF_HEADER)); } _rowCount = tableBuffer.getInt(getFormat().OFFSET_NUM_ROWS); - _lastAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER); + _lastLongAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER); _tableType = tableBuffer.get(getFormat().OFFSET_TABLE_TYPE); _maxColumnCount = tableBuffer.getShort(getFormat().OFFSET_MAX_COLS); _maxVarColumnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_VAR_COLS); @@ -1291,7 +1287,7 @@ public class Table // make sure rowcount and autonumber are up-to-date _rowCount += rowCountInc; tdefPage.putInt(getFormat().OFFSET_NUM_ROWS, _rowCount); - tdefPage.putInt(getFormat().OFFSET_NEXT_AUTO_NUMBER, _lastAutoNumber); + tdefPage.putInt(getFormat().OFFSET_NEXT_AUTO_NUMBER, _lastLongAutoNumber); // write any index changes Iterator indIter = _indexes.iterator(); @@ -1369,7 +1365,7 @@ public class Table if(col.isAutoNumber()) { // ignore given row value, use next autonumber - rowValue = getNextAutoNumber(); + rowValue = col.getAutoNumberGenerator().getNext(); // we need to stick this back in the row so that the indexes get // updated correctly (and caller can get the generated value) @@ -1468,14 +1464,14 @@ public class Table return _rowCount; } - private int getNextAutoNumber() { + int getNextLongAutoNumber() { // note, the saved value is the last one handed out, so pre-increment - return ++_lastAutoNumber; + return ++_lastLongAutoNumber; } - int getLastAutoNumber() { + int getLastLongAutoNumber() { // gets the last used auto number (does not modify) - return _lastAutoNumber; + return _lastLongAutoNumber; } @Override @@ -1660,17 +1656,16 @@ public class Table } /** - * @return the number of "AutoNumber" columns in the given collection of - * columns. + * @return the "AutoNumber" columns in the given collection of columns. */ - public static int countAutoNumberColumns(Collection columns) { - int numAutoNumCols = 0; + public static List getAutoNumberColumns(Collection columns) { + List autoCols = new ArrayList(); for(Column c : columns) { if(c.isAutoNumber()) { - ++numAutoNumCols; + autoCols.add(c); } } - return numAutoNumCols; + return autoCols; } /** diff --git a/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java b/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java index 5b5d343..9055ed8 100644 --- a/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -1032,6 +1032,20 @@ public class DatabaseTest extends TestCase { } } + static void dumpIndex(Index index) throws Exception { + dumpIndex(index, new PrintWriter(System.out, true)); + } + + static void dumpIndex(Index index, PrintWriter writer) throws Exception { + writer.println("INDEX: " + index); + Index.EntryCursor ec = index.cursor(); + Index.Entry lastE = ec.getLastEntry(); + Index.Entry e = null; + while((e = ec.getNextEntry()) != lastE) { + writer.println(e); + } + } + static void copyFile(File srcFile, File dstFile) throws IOException {