<author email="javajedi@users.sf.net">Tim McCune</author>
</properties>
<body>
+ <release version="1.1.20" date="TBD">
+ <action dev="jahlborn" type="fix" issue="2884599">
+ Add support for updating GUID indexes and for auto-number GUID
+ fields.
+ </action>
+ </release>
<release version="1.1.19" date="2009-06-13">
<action dev="jahlborn" type="add">
Add Query reading support.
* @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 {
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;
+ }
}
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;
/** 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;
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);
}
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);
public void setAutoNumber(boolean autoNumber) {
_autoNumber = autoNumber;
+ setAutoNumberGenerator();
}
public short getColumnNumber() {
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.
*
}
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");
}
}
} 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
/**
* 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,
/**
* 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);
}
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
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();
{
// 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);
}
}
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;
+ }
+ }
+
}
UNKNOWN_0D((byte) 0x0D, null, null, true, false, 0, 255, 255, 1),
/**
* Corresponds to a java String with the pattern
- * <code>"{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"</code>. Accepts any
+ * <code>"{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"</code>, 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}.
*/
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;
}
}
- if(Table.countAutoNumberColumns(columns) > 1) {
- throw new IllegalArgumentException(
- "Can have at most one AutoNumber column per table");
+ List<Column> autoCols = Table.getAutoNumberColumns(columns);
+ if(autoCols.size() > 1) {
+ // we can have one of each type
+ Set<DataType> 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.
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
}
}
+ /**
+ * 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.
*/
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;
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) */
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);
}
flags |= Column.FIXED_LEN_FLAG_MASK;
}
if(col.isAutoNumber()) {
- flags |= Column.AUTO_NUMBER_FLAG_MASK;
+ flags |= col.getAutoNumberGenerator().getColumnFlags();
}
return flags;
}
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);
// 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<Index> indIter = _indexes.iterator();
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)
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
}
/**
- * @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<Column> columns) {
- int numAutoNumCols = 0;
+ public static List<Column> getAutoNumberColumns(Collection<Column> columns) {
+ List<Column> autoCols = new ArrayList<Column>();
for(Column c : columns) {
if(c.isAutoNumber()) {
- ++numAutoNumCols;
+ autoCols.add(c);
}
}
- return numAutoNumCols;
+ return autoCols;
}
/**
}
}
+ 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
{