]> source.dussan.org Git - jackcess.git/commitdiff
initial support for guid indexes and guid autonumbers
authorJames Ahlborn <jtahlborn@yahoo.com>
Tue, 27 Oct 2009 01:26:46 +0000 (01:26 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Tue, 27 Oct 2009 01:26:46 +0000 (01:26 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@406 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/ByteUtil.java
src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/DataType.java
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/Index.java
src/java/com/healthmarketscience/jackcess/IndexCodes.java
src/java/com/healthmarketscience/jackcess/Table.java
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java

index 8416262300e822889da001d584cc9e67e5a40f00..2be53cf0c210b44e40d6f3354bcb332e1b70f23d 100644 (file)
@@ -4,6 +4,12 @@
     <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.
index 0ea90ce96e253dfd01d24ff7c5064997404e4082..95c2d8eeab7873172c0bc1bf5a70d05f2713330d 100644 (file)
@@ -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;
+  }
   
 }
index ea5b9c9762d668b0a727be7f3223dced4e0083cc..9f689b32c445b133cf436dbe958d137eac6d72d1 100644 (file)
@@ -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<Column> {
   /** 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<Column> {
   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<Column> {
     }
     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<Column> {
 
   public void setAutoNumber(boolean autoNumber) {
     _autoNumber = autoNumber;
+    setAutoNumberGenerator();
   }
 
   public short getColumnNumber() {
@@ -323,6 +332,29 @@ public class Column implements Comparable<Column> {
     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<Column> {
     }
 
     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<Column> {
     } 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<Column> {
   /**
    * 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<Column> {
   /**
    * 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<Column> {
       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<Column> {
       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<Column> {
   {
     // 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<Column> {
       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;
+    }
+  }
+
 }
index b2e2178e05e2b9aed4b18665cff43bdc40d96d86..0537103c8bca5c80a88165963d6d9ae73c773185 100644 (file)
@@ -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
-   * <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}.
    */
index 71380cec07ae498e113a92697e5320eeb4f48f7a..d0aa45b17e51b7cab9834277f4c945fdefdbcd3f 100644 (file)
@@ -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<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.
index 536389445b0135013fc6b2a2ab64924ad8702345..97315d6b36dbb5d29e7f791fc5706479bc73a8fc 100644 (file)
@@ -1186,6 +1186,8 @@ public abstract class Index implements Comparable<Index> {
       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<Index> {
     }    
   }
 
+  /**
+   * 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.
    */
index a3c254dd5d4a9d3eef75480dc04c5477fb2b6503..88aa37cbebb128ee5e266ac8e315fac44080a3f6 100644 (file)
@@ -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;
   
index ecd3edf7cd3af8aa57cee74f50cadbaac91153f6..585a619a4fafa729f22c46cbd8c06cf7ddff2423 100644 (file)
@@ -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<Index> 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<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;
   }
 
   /**
index 5b5d343c934ae376e24ab950bbcb98425f877331..9055ed8f2fa4dbbbea1f07e1b73951b2fde768f8 100644 (file)
@@ -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
   {