]> source.dussan.org Git - jackcess.git/commitdiff
add (initial) read-only support for Access 97 (jet format 3) databases (#3003375...
authorJames Ahlborn <jtahlborn@yahoo.com>
Fri, 23 Jul 2010 04:36:09 +0000 (04:36 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Fri, 23 Jul 2010 04:36:09 +0000 (04:36 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@475 f203690c-595d-4dc9-a70b-905162fa7fd2

17 files changed:
src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/ByteUtil.java
src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/Index.java
src/java/com/healthmarketscience/jackcess/JetFormat.java
src/java/com/healthmarketscience/jackcess/Table.java
test/data/V1997/compIndexTestV1997.mdb [new file with mode: 0755]
test/data/V1997/delColTestV1997.mdb [new file with mode: 0755]
test/data/V1997/delTestV1997.mdb [new file with mode: 0755]
test/data/V1997/test2V1997.mdb [new file with mode: 0755]
test/src/java/com/healthmarketscience/jackcess/BigIndexTest.java
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java
test/src/java/com/healthmarketscience/jackcess/IndexTest.java
test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java
test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java
test/src/java/com/healthmarketscience/jackcess/TableTest.java

index 9a89d0a53c3e61b73625ca651cb4968f9ed39146..0b38dde1c8954c1f15584d02fb1502ff642903ba 100644 (file)
         Make Database.escapeIdentifier public; add methods to TableBuilder and
         ColumnBuilder to optionally escape names.
       </action>
+      <action dev="jahlborn" type="update" issue="2997751">
+        Add support for overriding charset and tiemzone used when
+        reading/writing database.
+      </action>
+      <action dev="jahlborn" type="add" issue="3003375">
+        Add support for reading Access 97 (jet format 3) databases.
+      </action>
     </release>
     <release version="1.2.0" date="2010-04-18">
       <action dev="bhamail" type="update" issue="1451628">
index 223b5b07cea326aaf03b2f2e53c7b078886b6849..5aa09eb8eedb0c17e8c34f7ca5fe4016c67b9cd7 100644 (file)
@@ -256,6 +256,41 @@ public final class ByteUtil {
     }
   }
 
+  /**
+   * Read an unsigned variable length int from a buffer
+   * @param buffer Buffer containing the variable length int
+   * @return The unsigned int
+   */
+  public static int getUnsignedVarInt(ByteBuffer buffer, int numBytes) {
+    int pos = buffer.position();
+    int rtn = getUnsignedVarInt(buffer, pos, numBytes);
+    buffer.position(pos + numBytes);
+    return rtn;
+  }
+
+  /**
+   * Read an unsigned variable length int from a buffer
+   * @param buffer Buffer containing the variable length int
+   * @param offset Offset at which to read the value
+   * @return The unsigned int
+   */
+  public static int getUnsignedVarInt(ByteBuffer buffer, int offset, 
+                                      int numBytes) {  
+    switch(numBytes) {
+    case 1:
+      return getUnsignedByte(buffer, offset);
+    case 2:
+      return getUnsignedShort(buffer, offset);
+    case 3:
+      return get3ByteInt(buffer, offset);
+    case 4:
+      return buffer.getInt(offset);
+    default:
+      throw new IllegalArgumentException("Invalid num bytes " + numBytes);
+    }
+  }
+
+  
   /**
    * Sets all bits in the given remaining byte range to 0.
    */
index 2290ebbf99b69b167b35cb1c702936f15ab197d8..71fb379abcc6ecf4902cb54b98dbe1a9ed94f42b 100644 (file)
@@ -39,12 +39,14 @@ import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.CharBuffer;
+import java.nio.charset.Charset;
 import java.sql.Blob;
 import java.sql.Clob;
 import java.sql.SQLException;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
+import java.util.TimeZone;
 import java.util.UUID;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -345,6 +347,14 @@ public class Column implements Comparable<Column> {
     return _fixedDataOffset;
   }
 
+  protected Charset getCharset() {
+    return getTable().getDatabase().getCharset();
+  }
+
+  protected TimeZone getTimeZone() {
+    return getTable().getDatabase().getTimeZone();
+  }
+
   private void setAutoNumberGenerator()
   {
     if(!_autoNumber || (_type == null)) {
@@ -732,7 +742,7 @@ public class Column implements Comparable<Column> {
   /**
    * Returns a java long time value converted from an access date double.
    */
-  private static long fromDateDouble(double value)
+  private long fromDateDouble(double value)
   {
     long time = Math.round(value * MILLISECONDS_PER_DAY);
     time -= MILLIS_BETWEEN_EPOCH_AND_1900;
@@ -763,7 +773,7 @@ public class Column implements Comparable<Column> {
    * Returns an access date double converted from a java Date/Calendar/Number
    * time value.
    */
-  private static double toDateDouble(Object value)
+  private double toDateDouble(Object value)
   {
       // seems access stores dates in the local timezone.  guess you just
       // hope you read it in the same timezone in which it was written!
@@ -780,9 +790,9 @@ public class Column implements Comparable<Column> {
   /**
    * Gets the timezone offset from UTC for the given time (including DST).
    */
-  private static long getTimeZoneOffset(long time)
+  private long getTimeZoneOffset(long time)
   {
-    Calendar c = Calendar.getInstance();
+    Calendar c = Calendar.getInstance(getTimeZone());
     c.setTimeInMillis(time);
     return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET));
   }  
@@ -1239,7 +1249,7 @@ public class Column implements Comparable<Column> {
         
       }
       
-      return decodeUncompressedText(data, getFormat());
+      return decodeUncompressedText(data, getCharset());
       
     } catch (IllegalInputException e) {
       throw (IOException)
@@ -1273,7 +1283,7 @@ public class Column implements Comparable<Column> {
     } else {
       // handle uncompressed data
       textBuf.append(decodeUncompressedText(data, dataStart, dataLength,
-                                            getFormat()));
+                                            getCharset()));
     }
   }
 
@@ -1282,9 +1292,9 @@ public class Column implements Comparable<Column> {
    * @return the decoded string
    */
   private static CharBuffer decodeUncompressedText(
-      byte[] textBytes, int startPos, int length, JetFormat format)
+      byte[] textBytes, int startPos, int length, Charset charset)
   {
-    return format.CHARSET.decode(ByteBuffer.wrap(textBytes, startPos, length));
+    return charset.decode(ByteBuffer.wrap(textBytes, startPos, length));
   }  
 
   /**
@@ -1320,7 +1330,7 @@ public class Column implements Comparable<Column> {
       }
     }
 
-    return encodeUncompressedText(text, getFormat());
+    return encodeUncompressedText(text, getCharset());
   }
 
   /**
@@ -1364,25 +1374,25 @@ public class Column implements Comparable<Column> {
   
   /**
    * @param textBytes bytes of text to decode
-   * @param format relevant db format
+   * @param charset relevant charset
    * @return the decoded string
    */
-  public static String decodeUncompressedText(byte[] textBytes,
-                                              JetFormat format)
+  public static String decodeUncompressedText(byte[] textBytes, 
+                                              Charset charset)
   {
-    return decodeUncompressedText(textBytes, 0, textBytes.length, format)
+    return decodeUncompressedText(textBytes, 0, textBytes.length, charset)
       .toString();
   }
 
   /**
    * @param text Text to encode
-   * @param format relevant db format
+   * @param db relevant db
    * @return A buffer with the text encoded
    */
   public static ByteBuffer encodeUncompressedText(CharSequence text,
-                                                  JetFormat format)
+                                                  Charset charset)
   {
-    return format.CHARSET.encode(CharBuffer.wrap(text));
+    return charset.encode(CharBuffer.wrap(text));
   }
 
   
index b445141a459b71ade392b8bc07012f2ea6a44333..215d4467d02a111ee8f0d3bf50424317a367e15c 100644 (file)
@@ -37,6 +37,7 @@ import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.ArrayList;
@@ -52,6 +53,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.TimeZone;
 import java.util.TreeSet;
 
 import com.healthmarketscience.jackcess.query.Query;
@@ -100,6 +102,16 @@ public class Database
   public static final String USE_BIG_INDEX_PROPERTY =
     "com.healthmarketscience.jackcess.bigIndex";
 
+  /** system property which can be used to set the default TimeZone used for
+      date calculations. */
+  public static final String TIMEZONE_PROPERTY =
+    "com.healthmarketscience.jackcess.timeZone";
+
+  /** system property prefix which can be used to set the default Charset
+      used for text data (full property includes the JetFormat version). */
+  public static final String CHARSET_PROPERTY_PREFIX =
+    "com.healthmarketscience.jackcess.charset.";
+
   /** default error handler used if none provided (just rethrows exception) */
   public static final ErrorHandler DEFAULT_ERROR_HANDLER = new ErrorHandler() {
       public Object handleRowError(Column column,
@@ -173,6 +185,7 @@ public class Database
   
   public static enum FileFormat {
 
+    V1997(null, JetFormat.VERSION_3),
     V2000("com/healthmarketscience/jackcess/empty.mdb", JetFormat.VERSION_4),
     V2003("com/healthmarketscience/jackcess/empty2003.mdb", JetFormat.VERSION_4),
     V2007("com/healthmarketscience/jackcess/empty2007.accdb", JetFormat.VERSION_5, ".accdb");
@@ -301,6 +314,10 @@ public class Database
   private ErrorHandler _dbErrorHandler;
   /** the file format of the database */
   private FileFormat _fileFormat;
+  /** charset to use when handling text */
+  private Charset _charset;
+  /** timezone to use when handling dates */
+  private TimeZone _timeZone;
   
   /**
    * Open an existing Database.  If the existing file is not writeable, the
@@ -355,13 +372,58 @@ public class Database
    */
   public static Database open(File mdbFile, boolean readOnly, boolean autoSync)
     throws IOException
+  {    
+    return open(mdbFile, readOnly, autoSync, null, null);
+  }
+
+  /**
+   * Open an existing Database.  If the existing file is not writeable or the
+   * readOnly flag is <code>true</code>, the file will be opened read-only.
+   * @param mdbFile File containing the database
+   * @param readOnly iff <code>true</code>, force opening file in read-only
+   *                 mode
+   * @param autoSync whether or not to enable auto-syncing on write.  if
+   *                 {@code true}, writes will be immediately flushed to disk.
+   *                 This leaves the database in a (fairly) consistent state
+   *                 on each write, but can be very inefficient for many
+   *                 updates.  if {@code false}, flushing to disk happens at
+   *                 the jvm's leisure, which can be much faster, but may
+   *                 leave the database in an inconsistent state if failures
+   *                 are encountered during writing.
+   * @param charset Charset to use, if {@code null}, uses default
+   * @param timeZone TimeZone to use, if {@code null}, uses default
+   */
+  public static Database open(File mdbFile, boolean readOnly, boolean autoSync,
+                              Charset charset, TimeZone timeZone)
+    throws IOException
   {    
     if(!mdbFile.exists() || !mdbFile.canRead()) {
       throw new FileNotFoundException("given file does not exist: " + mdbFile);
     }
-    return new Database(openChannel(mdbFile,
-                                    (!mdbFile.canWrite() || readOnly)),
-                        autoSync, null);
+
+    // force read-only for non-writable files
+    readOnly |= !mdbFile.canWrite();
+
+    // open file channel
+    FileChannel channel = openChannel(mdbFile, readOnly);
+
+    if(!readOnly) {
+
+      // verify that format supports writing
+      JetFormat jetFormat = JetFormat.getFormat(channel);
+
+      if(jetFormat.READ_ONLY) {
+        // shutdown the channel (quietly)
+        try {
+          channel.close();
+        } catch(Exception ignored) {
+          // we don't care
+        }
+        throw new IOException("jet format '" + jetFormat + "' does not support writing");
+      }
+    }
+
+    return new Database(channel, autoSync, null, charset, timeZone);
   }
   
   /**
@@ -438,12 +500,40 @@ public class Database
                                 boolean autoSync)
     throws IOException
   {
+    return create(fileFormat, mdbFile, autoSync, null, null);
+  }
+
+  /**
+   * Create a new Database for the given fileFormat
+   * @param fileFormat version of new database.
+   * @param mdbFile Location to write the new database to.  <b>If this file
+   *    already exists, it will be overwritten.</b>
+   * @param autoSync whether or not to enable auto-syncing on write.  if
+   *                 {@code true}, writes will be immediately flushed to disk.
+   *                 This leaves the database in a (fairly) consistent state
+   *                 on each write, but can be very inefficient for many
+   *                 updates.  if {@code false}, flushing to disk happens at
+   *                 the jvm's leisure, which can be much faster, but may
+   *                 leave the database in an inconsistent state if failures
+   *                 are encountered during writing.
+   * @param charset Charset to use, if {@code null}, uses default
+   * @param timeZone TimeZone to use, if {@code null}, uses default
+   */
+  public static Database create(FileFormat fileFormat, File mdbFile, 
+                                boolean autoSync, Charset charset,
+                                TimeZone timeZone)
+    throws IOException
+  {
+    if (fileFormat.getJetFormat().READ_ONLY) {
+      throw new IOException("jet format '" + fileFormat.getJetFormat() + "' does not support writing");
+    }
+
     FileChannel channel = openChannel(mdbFile, false);
     channel.truncate(0);
     channel.transferFrom(Channels.newChannel(
         Thread.currentThread().getContextClassLoader().getResourceAsStream(
             fileFormat._emptyFile)), 0, Integer.MAX_VALUE);
-    return new Database(channel, autoSync, fileFormat);
+    return new Database(channel, autoSync, fileFormat, charset, timeZone);
   }
 
   /**
@@ -471,17 +561,30 @@ public class Database
    * @param channel File channel of the database.  This needs to be a
    *    FileChannel instead of a ReadableByteChannel because we need to
    *    randomly jump around to various points in the file.
+   * @param autoSync whether or not to enable auto-syncing on write.  if
+   *                 {@code true}, writes will be immediately flushed to disk.
+   *                 This leaves the database in a (fairly) consistent state
+   *                 on each write, but can be very inefficient for many
+   *                 updates.  if {@code false}, flushing to disk happens at
+   *                 the jvm's leisure, which can be much faster, but may
+   *                 leave the database in an inconsistent state if failures
+   *                 are encountered during writing.
+   * @param fileFormat version of new database (if known)
+   * @param charset Charset to use, if {@code null}, uses default
+   * @param timeZone TimeZone to use, if {@code null}, uses default
    */
-  protected Database(FileChannel channel, boolean autoSync, 
-                     FileFormat fileFormat)
+  protected Database(FileChannel channel, boolean autoSync,
+                     FileFormat fileFormat, Charset charset, TimeZone timeZone)
     throws IOException
   {
     boolean success = false;
     try {
 
       _format = JetFormat.getFormat(channel);
+      _charset = ((charset == null) ? getDefaultCharset(_format) : charset);
       _fileFormat = fileFormat;
       _pageChannel = new PageChannel(channel, _format, autoSync);
+      _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone);
       // note, it's slighly sketchy to pass ourselves along partially
       // constructed, but only our _format and _pageChannel refs should be
       // needed
@@ -556,6 +659,44 @@ public class Database
     _dbErrorHandler = newErrorHandler;
   }    
 
+  /**
+   * Gets currently configured TimeZone (always non-{@code null}).
+   */
+  public TimeZone getTimeZone()
+  {
+    return _timeZone;
+  }
+
+  /**
+   * Sets a new TimeZone.  If {@code null}, resets to the value returned by
+   * {@link #getDefaultTimeZone}.
+   */
+  public void setTimeZone(TimeZone newTimeZone) {
+    if(newTimeZone == null) {
+      newTimeZone = getDefaultTimeZone();
+    }
+    _timeZone = newTimeZone;
+  }    
+
+  /**
+   * Gets currently configured Charset (always non-{@code null}).
+   */
+  public Charset getCharset()
+  {
+    return _charset;
+  }
+
+  /**
+   * Sets a new Charset.  If {@code null}, resets to the value returned by
+   * {@link #getDefaultCharset}.
+   */
+  public void setCharset(Charset newCharset) {
+    if(newCharset == null) {
+      newCharset = getDefaultCharset(getFormat());
+    }
+    _charset = newCharset;
+  }
+
   /**
    * Returns the FileFormat of this database (which may involve inspecting the
    * database itself).
@@ -747,7 +888,7 @@ public class Database
     
     //Write the tdef page to disk.
     int tdefPageNumber = Table.writeTableDefinition(columns, _pageChannel,
-                                                    _format);
+                                                    _format, getCharset());
     
     //Add this table to our internal list.
     addTable(name, Integer.valueOf(tdefPageNumber));
@@ -1255,6 +1396,47 @@ public class Database
     }
     return true;
   }
+
+  /**
+   * Returns the default TimeZone.  This is normally the platform default
+   * TimeZone as returned by {@link TimeZone#getDefault}, but can be
+   * overridden using the system property {@value #TIMEZONE_PROPERTY}.
+   */
+  public static TimeZone getDefaultTimeZone()
+  {
+    String tzProp = System.getProperty(TIMEZONE_PROPERTY);
+    if(tzProp != null) {
+      tzProp = tzProp.trim();
+      if(tzProp.length() > 0) {
+        return TimeZone.getTimeZone(tzProp);
+      }
+    }
+
+    // use system default
+    return TimeZone.getDefault();
+  }
+  
+  /**
+   * Returns the default Charset for the given JetFormat.  This may or may not
+   * be platform specific, depending on the format, but can be overridden
+   * using a system property composed of the prefix
+   * {@value #CHARSET_PROPERTY_PREFIX} followed by the JetFormat version to
+   * which the charset should apply, e.g. {@code
+   * "com.healthmarketscience.jackcess.charset.VERSION_3"}.
+   */
+  public static Charset getDefaultCharset(JetFormat format)
+  {
+    String csProp = System.getProperty(CHARSET_PROPERTY_PREFIX + format);
+    if(csProp != null) {
+      csProp = csProp.trim();
+      if(csProp.length() > 0) {
+        return Charset.forName(csProp);
+      }
+    }
+
+    // use format default
+    return format.CHARSET;
+  }
   
   /**
    * Utility class for storing table page number and actual name.
index a2921e4341715463cc503bad7afa77ca0d1710ad..32e1b42ba8090a88447f239a8dfacc9f9ae2d952 100644 (file)
@@ -366,9 +366,9 @@ public abstract class Index implements Comparable<Index> {
                                 umapRowNum, false);
 
     _rootPageNumber = tableBuffer.getInt();
-    tableBuffer.getInt(); //Forward past Unknown
+    ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_FLAGS); //Forward past Unknown
     _indexFlags = tableBuffer.get();
-    ByteUtil.forward(tableBuffer, 5);  //Forward past other stuff
+    ByteUtil.forward(tableBuffer, getFormat().SKIP_AFTER_INDEX_FLAGS); //Forward past other stuff
   }
 
   /**
index 56309a867e2152e0b570274374d711c124ba9dd0..8265d774c363ce0f3ae3dfb78c69bb6494f79f3e 100644 (file)
@@ -70,6 +70,9 @@ public abstract class JetFormat {
 
   // use nested inner class to avoid problematic static init loops
   private static final class PossibleFileFormats {
+    private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_3 = 
+      Collections.singletonMap(Database.FileFormat.V1997, (byte[])null);
+
     private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_4 = 
       new EnumMap<Database.FileFormat,byte[]>(Database.FileFormat.class);
 
@@ -89,6 +92,9 @@ public abstract class JetFormat {
   /** the name of this format */
   private final String _name;
   
+  /** the read/write mode of this format */
+  public final boolean READ_ONLY;
+  
   /** Database page size in bytes */
   public final int PAGE_SIZE;
   public final long MAX_DATABASE_SIZE;
@@ -120,6 +126,7 @@ public abstract class JetFormat {
   public final int OFFSET_COLUMN_LENGTH;
   public final int OFFSET_COLUMN_VARIABLE_TABLE_INDEX;
   public final int OFFSET_COLUMN_FIXED_DATA_OFFSET;
+  public final int OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET;
   
   public final int OFFSET_TABLE_DEF_LOCATION;
   
@@ -150,6 +157,14 @@ public abstract class JetFormat {
   public final int SIZE_TDEF_TRAILER;
   public final int SIZE_COLUMN_DEF_BLOCK;
   public final int SIZE_INDEX_ENTRY_MASK;
+  public final int SKIP_BEFORE_INDEX_FLAGS;
+  public final int SKIP_AFTER_INDEX_FLAGS;
+  public final int SKIP_BEFORE_INDEX_SLOT;
+  public final int SKIP_AFTER_INDEX_SLOT;
+  public final int SKIP_BEFORE_INDEX;
+  public final int SIZE_NAME_LENGTH;
+  public final int SIZE_ROW_COLUMN_COUNT;
+  public final int SIZE_ROW_VAR_COL_OFFSET;
   
   public final int USAGE_MAP_TABLE_BYTE_LENGTH;
 
@@ -162,6 +177,7 @@ public abstract class JetFormat {
   
   public final Charset CHARSET;
   
+  public static final JetFormat VERSION_3 = new Jet3Format();
   public static final JetFormat VERSION_4 = new Jet4Format();
   public static final JetFormat VERSION_5 = new Jet5Format();
 
@@ -178,19 +194,23 @@ public abstract class JetFormat {
     }
     buffer.flip();
     byte version = buffer.get();
-    if (version == CODE_VERSION_4) {
+    if (version == CODE_VERSION_3) {
+      return VERSION_3;
+    } else if (version == CODE_VERSION_4) {
       return VERSION_4;
     } else if (version == CODE_VERSION_5) {
       return VERSION_5;
     }
     throw new IOException("Unsupported " +
-                          ((version < CODE_VERSION_4) ? "older" : "newer") +
+                          ((version < CODE_VERSION_3) ? "older" : "newer") +
                           " version: " + version);
   }
   
   private JetFormat(String name) {
     _name = name;
     
+    READ_ONLY = defineReadOnly();
+    
     PAGE_SIZE = definePageSize();
     MAX_DATABASE_SIZE = defineMaxDatabaseSize();
     
@@ -221,6 +241,7 @@ public abstract class JetFormat {
     OFFSET_COLUMN_LENGTH = defineOffsetColumnLength();
     OFFSET_COLUMN_VARIABLE_TABLE_INDEX = defineOffsetColumnVariableTableIndex();
     OFFSET_COLUMN_FIXED_DATA_OFFSET = defineOffsetColumnFixedDataOffset();
+    OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET = defineOffsetColumnFixedDataRowOffset();
     
     OFFSET_TABLE_DEF_LOCATION = defineOffsetTableDefLocation();
     
@@ -251,7 +272,15 @@ public abstract class JetFormat {
     SIZE_TDEF_TRAILER = defineSizeTdefTrailer();
     SIZE_COLUMN_DEF_BLOCK = defineSizeColumnDefBlock();
     SIZE_INDEX_ENTRY_MASK = defineSizeIndexEntryMask();
-    
+    SKIP_BEFORE_INDEX_FLAGS = defineSkipBeforeIndexFlags();
+    SKIP_AFTER_INDEX_FLAGS = defineSkipAfterIndexFlags();
+    SKIP_BEFORE_INDEX_SLOT = defineSkipBeforeIndexSlot();
+    SKIP_AFTER_INDEX_SLOT = defineSkipAfterIndexSlot();
+    SKIP_BEFORE_INDEX = defineSkipBeforeIndex();
+    SIZE_NAME_LENGTH = defineSizeNameLength();
+    SIZE_ROW_COLUMN_COUNT = defineSizeRowColumnCount();
+    SIZE_ROW_VAR_COL_OFFSET = defineSizeRowVarColOffset();
+
     USAGE_MAP_TABLE_BYTE_LENGTH = defineUsageMapTableByteLength();
 
     MAX_COLUMNS_PER_TABLE = defineMaxColumnsPerTable();
@@ -264,6 +293,8 @@ public abstract class JetFormat {
     CHARSET = defineCharset();
   }
   
+  protected abstract boolean defineReadOnly();
+  
   protected abstract int definePageSize();
   protected abstract long defineMaxDatabaseSize();
   
@@ -294,6 +325,7 @@ public abstract class JetFormat {
   protected abstract int defineOffsetColumnLength();
   protected abstract int defineOffsetColumnVariableTableIndex();
   protected abstract int defineOffsetColumnFixedDataOffset();
+  protected abstract int defineOffsetColumnFixedDataRowOffset();
   
   protected abstract int defineOffsetTableDefLocation();
   
@@ -324,7 +356,15 @@ public abstract class JetFormat {
   protected abstract int defineSizeTdefTrailer();
   protected abstract int defineSizeColumnDefBlock();
   protected abstract int defineSizeIndexEntryMask();
-  
+  protected abstract int defineSkipBeforeIndexFlags();
+  protected abstract int defineSkipAfterIndexFlags();
+  protected abstract int defineSkipBeforeIndexSlot();
+  protected abstract int defineSkipAfterIndexSlot();
+  protected abstract int defineSkipBeforeIndex();
+  protected abstract int defineSizeNameLength();
+  protected abstract int defineSizeRowColumnCount();
+  protected abstract int defineSizeRowVarColOffset();
+
   protected abstract int defineUsageMapTableByteLength();
 
   protected abstract int defineMaxColumnsPerTable();
@@ -343,6 +383,175 @@ public abstract class JetFormat {
     return _name;
   }
   
+  private static class Jet3Format extends JetFormat {
+
+    private Jet3Format() {
+      super("VERSION_3");
+    }
+
+    @Override
+    protected boolean defineReadOnly() { return true; }
+           
+    @Override
+    protected int definePageSize() { return 2048; }
+           
+    @Override
+    protected long defineMaxDatabaseSize() {
+      return (1L * 1024L * 1024L * 1024L);
+    }
+           
+    @Override
+    protected int defineMaxRowSize() { return 2012; }
+    @Override
+    protected int definePageInitialFreeSpace() { return PAGE_SIZE - 14; }
+           
+    @Override
+    protected int defineOffsetNextTableDefPage() { return 4; }
+    @Override
+    protected int defineOffsetNumRows() { return 12; }
+    @Override
+    protected int defineOffsetNextAutoNumber() { return 20; }
+    @Override
+    protected int defineOffsetTableType() { return 20; }
+    @Override
+    protected int defineOffsetMaxCols() { return 21; }
+    @Override
+    protected int defineOffsetNumVarCols() { return 23; }
+    @Override
+    protected int defineOffsetNumCols() { return 25; }
+    @Override
+    protected int defineOffsetNumIndexSlots() { return 27; }
+    @Override
+    protected int defineOffsetNumIndexes() { return 31; }
+    @Override
+    protected int defineOffsetOwnedPages() { return 35; }
+    @Override
+    protected int defineOffsetFreeSpacePages() { return 39; }
+    @Override
+    protected int defineOffsetIndexDefBlock() { return 43; }
+
+    @Override
+    protected int defineOffsetIndexNumberBlock() { return 39; }
+           
+    @Override
+    protected int defineOffsetColumnType() { return 0; }
+    @Override
+    protected int defineOffsetColumnNumber() { return 1; }
+    @Override
+    protected int defineOffsetColumnPrecision() { return 11; }
+    @Override
+    protected int defineOffsetColumnScale() { return 12; }
+    @Override
+    protected int defineOffsetColumnFlags() { return 13; }
+    @Override
+    protected int defineOffsetColumnCompressedUnicode() { return 16; }
+    @Override
+    protected int defineOffsetColumnLength() { return 16; }
+    @Override
+    protected int defineOffsetColumnVariableTableIndex() { return 3; }
+    @Override
+    protected int defineOffsetColumnFixedDataOffset() { return 14; }
+    @Override
+    protected int defineOffsetColumnFixedDataRowOffset() { return 1; }
+         
+    @Override
+    protected int defineOffsetTableDefLocation() { return 4; }
+           
+    @Override
+    protected int defineOffsetRowStart() { return 10; }
+    @Override
+    protected int defineOffsetUsageMapStart() { return 5; }
+           
+    @Override
+    protected int defineOffsetUsageMapPageData() { return 4; }
+           
+    @Override
+    protected int defineOffsetReferenceMapPageNumbers() { return 1; }
+           
+    @Override
+    protected int defineOffsetFreeSpace() { return 2; }
+    @Override
+    protected int defineOffsetNumRowsOnDataPage() { return 8; }
+    @Override
+    protected int defineMaxNumRowsOnDataPage() { return 255; }
+           
+    @Override
+    protected int defineOffsetIndexCompressedByteCount() { return 20; }
+    @Override
+    protected int defineOffsetIndexEntryMask() { return 22; }
+    @Override
+    protected int defineOffsetPrevIndexPage() { return 8; }
+    @Override
+    protected int defineOffsetNextIndexPage() { return 12; }
+    @Override
+    protected int defineOffsetChildTailIndexPage() { return 16; }
+           
+    @Override
+    protected int defineSizeIndexDefinition() { return 8; }
+    @Override
+    protected int defineSizeColumnHeader() { return 18; }
+    @Override
+    protected int defineSizeRowLocation() { return 2; }
+    @Override
+    protected int defineSizeLongValueDef() { return 12; }
+    @Override
+    protected int defineMaxInlineLongValueSize() { return 64; }
+    @Override
+    protected int defineMaxLongValueRowSize() { return 2032; }
+    @Override
+    protected int defineSizeTdefHeader() { return 63; }
+    @Override
+    protected int defineSizeTdefTrailer() { return 2; }
+    @Override
+    protected int defineSizeColumnDefBlock() { return 25; }
+    @Override
+    protected int defineSizeIndexEntryMask() { return 226; }
+    @Override
+    protected int defineSkipBeforeIndexFlags() { return 0; }
+    @Override
+    protected int defineSkipAfterIndexFlags() { return 0; }
+    @Override
+    protected int defineSkipBeforeIndexSlot() { return 0; }
+    @Override
+    protected int defineSkipAfterIndexSlot() { return 0; }
+    @Override
+    protected int defineSkipBeforeIndex() { return 0; }
+    @Override
+    protected int defineSizeNameLength() { return 1; }
+    @Override
+    protected int defineSizeRowColumnCount() { return 1; }
+    @Override
+    protected int defineSizeRowVarColOffset() { return 1; }
+           
+    @Override
+    protected int defineUsageMapTableByteLength() { return 128; }
+             
+    @Override
+    protected int defineMaxColumnsPerTable() { return 255; }
+             
+    @Override
+    protected int defineMaxTableNameLength() { return 64; }
+             
+    @Override
+    protected int defineMaxColumnNameLength() { return 64; }
+             
+    @Override
+    protected int defineMaxIndexNameLength() { return 64; }
+             
+    @Override
+    protected boolean defineReverseFirstByteInDescNumericIndexes() { return false; }
+
+    @Override
+    protected Charset defineCharset() { return Charset.defaultCharset(); }
+
+    @Override
+    protected Map<Database.FileFormat,byte[]> getPossibleFileFormats()
+    {
+      return PossibleFileFormats.POSSIBLE_VERSION_3;
+    }
+
+  }
+  
   private static class Jet4Format extends JetFormat {
 
     private Jet4Format() {
@@ -353,6 +562,9 @@ public abstract class JetFormat {
       super(name);
     }
 
+    @Override
+    protected boolean defineReadOnly() { return false; }
+    
     @Override
     protected int definePageSize() { return 4096; }
     
@@ -412,6 +624,8 @@ public abstract class JetFormat {
     protected int defineOffsetColumnVariableTableIndex() { return 7; }
     @Override
     protected int defineOffsetColumnFixedDataOffset() { return 21; }
+    @Override
+    protected int defineOffsetColumnFixedDataRowOffset() { return 2; }
   
     @Override
     protected int defineOffsetTableDefLocation() { return 4; }
@@ -465,6 +679,22 @@ public abstract class JetFormat {
     protected int defineSizeColumnDefBlock() { return 25; }
     @Override
     protected int defineSizeIndexEntryMask() { return 453; }
+    @Override
+    protected int defineSkipBeforeIndexFlags() { return 4; }
+    @Override
+    protected int defineSkipAfterIndexFlags() { return 5; }
+    @Override
+    protected int defineSkipBeforeIndexSlot() { return 4; }
+    @Override
+    protected int defineSkipAfterIndexSlot() { return 4; }
+    @Override
+    protected int defineSkipBeforeIndex() { return 4; }
+    @Override
+    protected int defineSizeNameLength() { return 2; }
+    @Override
+    protected int defineSizeRowColumnCount() { return 2; }
+    @Override
+    protected int defineSizeRowVarColOffset() { return 2; }
     
     @Override
     protected int defineUsageMapTableByteLength() { return 64; }
index 02120c109a583f195cd5527b8e80ee64489a5120..36f7c668665320f57dc1c4eae62e59f0b3ecb474 100644 (file)
@@ -29,6 +29,7 @@ package com.healthmarketscience.jackcess;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -61,6 +62,8 @@ public class Table
   
   private static final short OVERFLOW_ROW_MASK = (short)0x4000;
 
+  private static final int MAX_BYTE = 256;
+
   /** Table type code for system tables */
   public static final byte TYPE_SYSTEM = 0x53;
   /** Table type code for user tables */
@@ -403,7 +406,7 @@ public class Table
     ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
     requireNonDeletedRow(rowState, rowId);
     
-    return getRowColumn(rowBuffer, getRowNullMask(rowBuffer), column,
+    return getRowColumn(getFormat(), rowBuffer, getRowNullMask(rowBuffer), column,
                         rowState);
   }
 
@@ -421,7 +424,7 @@ public class Table
     ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
     requireNonDeletedRow(rowState, rowId);
 
-    return getRow(rowState, rowBuffer, getRowNullMask(rowBuffer), _columns,
+    return getRow(getFormat(), rowState, rowBuffer, getRowNullMask(rowBuffer), _columns,
                   columnNames);
   }
 
@@ -430,6 +433,7 @@ public class Table
    * Saves parsed row values to the given rowState.
    */
   private static Map<String, Object> getRow(
+         JetFormat format,
       RowState rowState,
       ByteBuffer rowBuffer,
       NullMask nullMask,
@@ -444,7 +448,7 @@ public class Table
       if((columnNames == null) || (columnNames.contains(column.getName()))) {
         // Add the value to the row data
         rtn.put(column.getName(), 
-                getRowColumn(rowBuffer, nullMask, column, rowState));
+                getRowColumn(format, rowBuffer, nullMask, column, rowState));
       }
     }
     return rtn;
@@ -454,7 +458,8 @@ public class Table
    * Reads the column data from the given row buffer.  Leaves limit unchanged.
    * Caches the returned value in the rowState.
    */
-  private static Object getRowColumn(ByteBuffer rowBuffer,
+  private static Object getRowColumn(JetFormat format,
+                                     ByteBuffer rowBuffer,
                                      NullMask nullMask,
                                      Column column,
                                      RowState rowState)
@@ -484,19 +489,34 @@ public class Table
       if(!column.isVariableLength()) {
 
         // read fixed length value (non-boolean at this point)
-        int dataStart = rowStart + 2;
+        int dataStart = rowStart + format.OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET;
         colDataPos = dataStart + column.getFixedDataOffset();
         colDataLen = column.getType().getFixedSize(column.getLength());
       
       } else {
+        int varDataStart; 
+        int varDataEnd;
+
+        if(format.SIZE_ROW_VAR_COL_OFFSET == 2) {
+
+          // read simple var length value
+          int varColumnOffsetPos =
+            (rowBuffer.limit() - nullMask.byteSize() - 4) -
+            (column.getVarLenTableIndex() * 2);
+
+          varDataStart = rowBuffer.getShort(varColumnOffsetPos);
+          varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2);
+
+        } else {
 
-        // read var length value
-        int varColumnOffsetPos =
-          (rowBuffer.limit() - nullMask.byteSize() - 4) -
-          (column.getVarLenTableIndex() * 2);
+          // read jump-table based var length values
+          short[] varColumnOffsets = readJumpTableVarColOffsets(
+              rowState, rowBuffer, rowStart, nullMask);
+
+          varDataStart = varColumnOffsets[column.getVarLenTableIndex()];
+          varDataEnd = varColumnOffsets[column.getVarLenTableIndex() + 1];
+        }
 
-        short varDataStart = rowBuffer.getShort(varColumnOffsetPos);
-        short varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2);
         colDataPos = rowStart + varDataStart;
         colDataLen = varDataEnd - varDataStart;
       }
@@ -523,16 +543,61 @@ public class Table
     }
   }
 
+  static short[] readJumpTableVarColOffsets(
+      RowState rowState, ByteBuffer rowBuffer, int rowStart,
+      NullMask nullMask) 
+  {
+    short[] varColOffsets = rowState.getVarColOffsets();
+    if(varColOffsets != null) {
+      return varColOffsets;
+    }
+
+    // calculate offsets using jump-table info
+    int nullMaskSize = nullMask.byteSize();
+    int rowEnd = rowStart + rowBuffer.remaining() - 1;
+    int numVarCols = ByteUtil.getUnsignedByte(rowBuffer, 
+                                              rowEnd - nullMaskSize);
+    varColOffsets = new short[numVarCols + 1];
+         
+    int rowLen = rowEnd - rowStart + 1;
+    int numJumps = (rowLen - 1) / MAX_BYTE;
+    int colOffset = rowEnd - nullMaskSize - numJumps - 1;
+         
+    // If last jump is a dummy value, ignore it
+    if(((colOffset - rowStart - numVarCols) / MAX_BYTE) < numJumps) {
+      numJumps--;
+    }
+
+    int jumpsUsed = 0;
+    for(int i = 0; i < numVarCols + 1; i++) {
+
+      if((jumpsUsed < numJumps) && 
+         (i == ByteUtil.getUnsignedByte(
+              rowBuffer, rowEnd - nullMaskSize-jumpsUsed - 1))) {
+        jumpsUsed++;
+      }
+                 
+      varColOffsets[i] = (short)
+        (ByteUtil.getUnsignedByte(rowBuffer, colOffset - i)
+         + (jumpsUsed * MAX_BYTE));
+    }
+         
+    rowState.setVarColOffsets(varColOffsets);
+    return varColOffsets;
+  }
+
   /**
    * Reads the null mask from the given row buffer.  Leaves limit unchanged.
    */
-  private static NullMask getRowNullMask(ByteBuffer rowBuffer)
+  private NullMask getRowNullMask(ByteBuffer rowBuffer)
     throws IOException
   {
     // reset position to row start
     rowBuffer.reset();
-    
-    short columnCount = rowBuffer.getShort(); // Number of columns in this row
+
+    // Number of columns in this row
+    int columnCount = ByteUtil.getUnsignedVarInt(
+        rowBuffer, getFormat().SIZE_ROW_COLUMN_COUNT);
     
     // read null mask
     NullMask nullMask = new NullMask(columnCount);
@@ -685,7 +750,8 @@ public class Table
    * @return the first page of the new table's definition
    */
   public static int writeTableDefinition(
-      List<Column> columns, PageChannel pageChannel, JetFormat format)
+      List<Column> columns, PageChannel pageChannel, JetFormat format,
+      Charset charset)
     throws IOException
   {
     // first, create the usage map page
@@ -710,7 +776,7 @@ public class Table
                                                           format.PAGE_SIZE));
     writeTableDefinitionHeader(buffer, columns, usageMapPageNumber,
                                totalTableDefSize, format);
-    writeColumnDefinitions(buffer, columns, format); 
+    writeColumnDefinitions(buffer, columns, format, charset); 
     
     //End of tabledef
     buffer.put((byte) 0xff);
@@ -834,7 +900,8 @@ public class Table
    * @param columns List of Columns to write definitions for
    */
   private static void writeColumnDefinitions(
-      ByteBuffer buffer, List<Column> columns, JetFormat format)
+      ByteBuffer buffer, List<Column> columns, JetFormat format,
+      Charset charset)
     throws IOException
   {
     short columnNumber = (short) 0;
@@ -896,7 +963,7 @@ public class Table
       }
     }
     for (Column col : columns) {
-      writeName(buffer, col.getName(), format);
+      writeName(buffer, col.getName(), charset);
     }
   }
 
@@ -905,10 +972,9 @@ public class Table
    * {@link #readName}.
    */
   private static void writeName(ByteBuffer buffer, String name,
-                                JetFormat format)
+                                Charset charset)
   {
-      ByteBuffer encName = Column.encodeUncompressedText(
-          name, format);
+      ByteBuffer encName = Column.encodeUncompressedText(name, charset);
       buffer.putShort((short) encName.remaining());
       buffer.put(encName);
   }
@@ -1041,12 +1107,12 @@ public class Table
     
     for (int i = 0; i < _indexSlotCount; i++) {
 
-      tableBuffer.getInt(); //Forward past Unknown
+      ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown
       tableBuffer.getInt(); //Forward past alternate index number
       int indexNumber = tableBuffer.getInt();
       ByteUtil.forward(tableBuffer, 11);
       byte indexType = tableBuffer.get();
-      ByteUtil.forward(tableBuffer, 4);
+      ByteUtil.forward(tableBuffer, getFormat().SKIP_AFTER_INDEX_SLOT); //Skip past Unknown
 
       if(i < firstRealIdx) {
         // ignore this info
@@ -1077,7 +1143,7 @@ public class Table
     // go back to index column info after sorting
     tableBuffer.position(idxOffset);
     for (int i = 0; i < _indexCount; i++) {
-      tableBuffer.getInt(); //Forward past Unknown
+      ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX); //Forward past Unknown
       _indexes.get(i).read(tableBuffer, _columns);
     }
 
@@ -1115,15 +1181,16 @@ public class Table
   }
 
   /**
-   * Returns a name read from the buffer at the current position.  The
-   * expected name format is the name length as a short followed by (length *
-   * 2) bytes encoded using the {@link JetFormat#CHARSET}
+   * Returns a name read from the buffer at the current position. The
+   * expected name format is the name length followed by the name 
+   * encoded using the {@link JetFormat#CHARSET}
    */
-  private String readName(ByteBuffer buffer) {
-    int nameLength = ByteUtil.getUnsignedShort(buffer);
+  private String readName(ByteBuffer buffer) { 
+    int nameLength = readNameLength(buffer);
     byte[] nameBytes = new byte[nameLength];
     buffer.get(nameBytes);
-    return Column.decodeUncompressedText(nameBytes, getFormat());
+    return Column.decodeUncompressedText(nameBytes, 
+                                         getDatabase().getCharset());
   }
   
   /**
@@ -1131,10 +1198,17 @@ public class Table
    * expected name format is the same as that for {@link #readName}.
    */
   private void skipName(ByteBuffer buffer) {
-    int nameLength = ByteUtil.getUnsignedShort(buffer);
+       int nameLength = readNameLength(buffer);
     ByteUtil.forward(buffer, nameLength);
   }
   
+  /**
+   * Returns a name length read from the buffer at the current position.
+   */
+  private int readNameLength(ByteBuffer buffer) { 
+    return ByteUtil.getUnsignedVarInt(buffer, getFormat().SIZE_NAME_LENGTH);
+  }
+  
   /**
    * Converts a map of columnName -> columnValue to an array of row values
    * appropriate for a call to {@link #addRow(Object...)}.
@@ -1307,7 +1381,7 @@ public class Table
     for(Column column : _columns) {
       if(column.isAutoNumber() || 
          (row[column.getColumnIndex()] == Column.KEEP_VALUE)) {
-        row[column.getColumnIndex()] = getRowColumn(rowBuffer, nullMask,
+        row[column.getColumnIndex()] = getRowColumn(getFormat(), rowBuffer, nullMask,
                                                     column, rowState);
       }
     }
@@ -1929,6 +2003,8 @@ public class Table
     private int _lastModCount;
     /** optional error handler to use when row errors are encountered */
     private ErrorHandler _errorHandler;
+    /** cached variable column offsets for jump-table based rows */
+    private short[] _varColOffsets;
   
     private RowState(TempBufferHolder.Type headerType) {
       _headerRowBufferH = TempPageHolder.newHolder(headerType);
@@ -1955,6 +2031,7 @@ public class Table
       _rowsOnHeaderPage = 0;
       _status = RowStateStatus.INIT;
       _rowStatus = RowStatus.INIT;
+      _varColOffsets = null;
       if(_haveRowValues) {
         Arrays.fill(_rowValues, null);
         _haveRowValues = false;
@@ -2036,6 +2113,14 @@ public class Table
     public Object[] getRowValues() {
       return dupeRow(_rowValues, _rowValues.length);
     }
+
+    private short[] getVarColOffsets() {
+      return _varColOffsets;
+    }
+
+    private void setVarColOffsets(short[] varColOffsets) {
+      _varColOffsets = varColOffsets;
+    }
     
     public RowId getHeaderRowId() {
       return _headerRowId;
diff --git a/test/data/V1997/compIndexTestV1997.mdb b/test/data/V1997/compIndexTestV1997.mdb
new file mode 100755 (executable)
index 0000000..6121fc3
Binary files /dev/null and b/test/data/V1997/compIndexTestV1997.mdb differ
diff --git a/test/data/V1997/delColTestV1997.mdb b/test/data/V1997/delColTestV1997.mdb
new file mode 100755 (executable)
index 0000000..9534e76
Binary files /dev/null and b/test/data/V1997/delColTestV1997.mdb differ
diff --git a/test/data/V1997/delTestV1997.mdb b/test/data/V1997/delTestV1997.mdb
new file mode 100755 (executable)
index 0000000..add3763
Binary files /dev/null and b/test/data/V1997/delTestV1997.mdb differ
diff --git a/test/data/V1997/test2V1997.mdb b/test/data/V1997/test2V1997.mdb
new file mode 100755 (executable)
index 0000000..b8b2ca2
Binary files /dev/null and b/test/data/V1997/test2V1997.mdb differ
index c0ff64b99f7c731867d272545742224c09b79625..4f79602b006f44d4ea9bc95f21834a9863bb5e25 100644 (file)
@@ -67,7 +67,7 @@ public class BigIndexTest extends TestCase {
   
   public void testComplexIndex() throws Exception
   {
-    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMP_INDEX)) {
+    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMP_INDEX, true)) {
       // this file has an index with "compressed" entries and node pages
       Database db = open(testDB);
       Table t = db.getTable("Table1");
index 4601cfaa9eb4296d9f902722ec352f377304f11e..1f993ee81613ac28da6722ac05fa46cba124744a 100644 (file)
@@ -176,7 +176,7 @@ public class DatabaseTest extends TestCase {
   }
       
   public void testReadDeletedRows() throws Exception {
-    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL)) {
+    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL, true)) {
       Table table = open(testDB).getTable("Table");
       int rows = 0;
       while (table.getNextRow() != null) {
@@ -187,7 +187,7 @@ public class DatabaseTest extends TestCase {
   }
   
   public void testGetColumns() throws Exception {
-    for (final TestDB testDB : SUPPORTED_DBS_TEST) {
+    for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
 
       List<Column> columns = open(testDB).getTable("Table1").getColumns();
       assertEquals(9, columns.size());
@@ -213,7 +213,7 @@ public class DatabaseTest extends TestCase {
   }
   
   public void testGetNextRow() throws Exception {
-    for (final TestDB testDB : SUPPORTED_DBS_TEST) {
+    for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
       final Database db = open(testDB);
       assertEquals(4, db.getTableNames().size());
       final Table table = db.getTable("Table1");
@@ -272,7 +272,7 @@ public class DatabaseTest extends TestCase {
     assertEquals(Boolean.FALSE, row.get("I"));
   }
 
-    public void testCreate() throws Exception {
+  public void testCreate() throws Exception {
     for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
       Database db = create(fileFormat);
       assertEquals(0, db.getTableNames().size());
@@ -396,7 +396,7 @@ public class DatabaseTest extends TestCase {
 
   public void testReadLongValue() throws Exception {
 
-    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.TEST2)) {
+    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.TEST2, true)) {
       Database db = open(testDB);
       Table table = db.getTable("MSP_PROJECTS");
       Map<String, Object> row = table.getNextRow();
@@ -528,7 +528,7 @@ public class DatabaseTest extends TestCase {
   }
 
   public void testReadWithDeletedCols() throws Exception {
-    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL_COL)) {
+    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL_COL, true)) {
       Table table = open(testDB).getTable("Table1");
 
       Map<String, Object> expectedRow0 = new LinkedHashMap<String, Object>();
@@ -733,7 +733,7 @@ public class DatabaseTest extends TestCase {
 
   public void testMultiPageTableDef() throws Exception
   {
-    for (final TestDB testDB : SUPPORTED_DBS_TEST) {
+    for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
       List<Column> columns = open(testDB).getTable("Table2").getColumns();
       assertEquals(89, columns.size());
     }
index adf9985c05f7f42491b3a1310c63fff56c0c98d2..504a3b822ab3b22485db524cbcbce41057bcf85d 100644 (file)
@@ -84,7 +84,7 @@ public class IndexTest extends TestCase {
   }
 
   public void testPrimaryKey() throws Exception {
-    for (final TestDB testDB : SUPPORTED_DBS_TEST) {
+    for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
       Table table = open(testDB).getTable("Table1");
       Map<String, Boolean> foundPKs = new HashMap<String, Boolean>();
       for(Index index : table.getIndexes()) {
@@ -100,7 +100,7 @@ public class IndexTest extends TestCase {
   
   public void testIndexSlots() throws Exception
   {
-    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX)) {
+    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) {
       Database mdb = open(testDB);
 
       Table table = mdb.getTable("Table1");
index 44c5544f2729f430dfcafe1ecc5140d2cde63a71..0c315199df5d500734b330492c612099e0c31861 100644 (file)
@@ -4,7 +4,6 @@ import java.io.File;
 import java.io.IOException;
 import java.nio.channels.FileChannel;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
@@ -12,6 +11,7 @@ import java.util.Set;
 import junit.framework.TestCase;
 
 import static com.healthmarketscience.jackcess.Database.*;
+import static com.healthmarketscience.jackcess.DatabaseTest.*;
 
 /**
  * @author Dan Rollo
@@ -28,21 +28,20 @@ public class JetFormatTest extends TestCase {
   public static enum Basename {
 
     BIG_INDEX("bigIndexTest"),
-      COMP_INDEX("compIndexTest"),
-      DEL_COL("delColTest"),
-      DEL("delTest"),
-      FIXED_NUMERIC("fixedNumericTest"),
-      FIXED_TEXT("fixedTextTest"),
-      INDEX_CURSOR("indexCursorTest"),
-      INDEX("indexTest"),
-      OVERFLOW("overflowTest"),
-      QUERY("queryTest"),
-      TEST("test"),
-      TEST2("test2"),
-      INDEX_CODES("testIndexCodes"),
-      INDEX_PROPERTIES("testIndexProperties"),
-      PROMOTION("testPromotion"),
-      ;
+    COMP_INDEX("compIndexTest"),
+    DEL_COL("delColTest"),
+    DEL("delTest"),
+    FIXED_NUMERIC("fixedNumericTest"),
+    FIXED_TEXT("fixedTextTest"),
+    INDEX_CURSOR("indexCursorTest"),
+    INDEX("indexTest"),
+    OVERFLOW("overflowTest"),
+    QUERY("queryTest"),
+    TEST("test"),
+    TEST2("test2"),
+    INDEX_CODES("testIndexCodes"),
+    INDEX_PROPERTIES("testIndexProperties"),
+    PROMOTION("testPromotion");
 
     private final String _basename;
 
@@ -58,6 +57,7 @@ public class JetFormatTest extends TestCase {
       runtime via the system property
       "com.healthmarketscience.jackcess.testFormats") */
   final static FileFormat[] SUPPORTED_FILEFORMATS;
+  final static FileFormat[] SUPPORTED_FILEFORMATS_FOR_READ;
 
   static {
     String testFormatStr = System.getProperty("com.healthmarketscience.jackcess.testFormats");
@@ -70,15 +70,21 @@ public class JetFormatTest extends TestCase {
     }
 
     List<FileFormat> supported = new ArrayList<FileFormat>();
-    for(FileFormat ff : Arrays.asList(FileFormat.V2000, FileFormat.V2003,
-                                      FileFormat.V2007)) {
+    List<FileFormat> supportedForRead = new ArrayList<FileFormat>();
+    for(FileFormat ff : FileFormat.values()) {
       if(!testFormats.contains(ff)) {
         continue;
       }
+      supportedForRead.add(ff);
+      if(ff.getJetFormat().READ_ONLY) {
+        continue;
+      }
       supported.add(ff);
     }
 
     SUPPORTED_FILEFORMATS = supported.toArray(new FileFormat[0]);
+    SUPPORTED_FILEFORMATS_FOR_READ = 
+      supportedForRead.toArray(new FileFormat[0]);
   }
 
   /**
@@ -113,12 +119,36 @@ public class JetFormatTest extends TestCase {
     }
 
     public static List<TestDB> getSupportedForBasename(Basename basename) {
+      return getSupportedForBasename(basename, false);
+    }
+
+    public static List<TestDB> getSupportedForBasename(Basename basename,
+                                                       boolean readOnly) {
 
       List<TestDB> supportedTestDBs = new ArrayList<TestDB>();
-      for (FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
-        supportedTestDBs.add(new TestDB(
-                                 getFileForBasename(basename, fileFormat),
-                                 fileFormat));
+      for (FileFormat fileFormat : 
+             (readOnly ? SUPPORTED_FILEFORMATS_FOR_READ :
+              SUPPORTED_FILEFORMATS)) {
+        File testFile = getFileForBasename(basename, fileFormat);
+        if(!testFile.exists()) {
+          continue;
+        }
+        
+        // verify that the db is the file format expected
+        try {
+//           System.out.println("FOO checking " + testFile);
+          Database db = Database.open(testFile, true);
+          FileFormat dbFileFormat = db.getFileFormat();
+          db.close();
+          if(dbFileFormat != fileFormat) {
+            throw new IllegalStateException("Expected " + fileFormat +
+                                            " was " + dbFileFormat);
+          }
+        } catch(Exception e) {
+          throw new RuntimeException(e);
+        }
+
+        supportedTestDBs.add(new TestDB(testFile, fileFormat));
       }
       return supportedTestDBs;
     }
@@ -133,12 +163,10 @@ public class JetFormatTest extends TestCase {
     }
   }
 
-  private static final File UNSUPPORTED_TEST_V1997 = 
-    new File(DIR_TEST_DATA, "V1997" + File.separator +
-             Basename.TEST + "V1997.mdb");
-
   static final List<TestDB> SUPPORTED_DBS_TEST = 
     TestDB.getSupportedForBasename(Basename.TEST);
+  static final List<TestDB> SUPPORTED_DBS_TEST_FOR_READ = 
+    TestDB.getSupportedForBasename(Basename.TEST, true);
 
 
   public void testGetFormat() throws Exception {
@@ -149,41 +177,61 @@ public class JetFormatTest extends TestCase {
       // success
     }
 
-    checkUnsupportedJetFormat(UNSUPPORTED_TEST_V1997);
+    for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
+
+      final FileChannel channel = Database.openChannel(testDB.dbFile, false);
+      try {
+
+        JetFormat fmtActual = JetFormat.getFormat(channel);
+        assertEquals("Unexpected JetFormat for dbFile: " + 
+                     testDB.dbFile.getAbsolutePath(),
+                     testDB.expectedFileFormat.getJetFormat(), fmtActual);
+
+      } finally {
+        channel.close();
+      }
 
-    for (final TestDB testDB : SUPPORTED_DBS_TEST) {
-      checkJetFormat(testDB);
     }
   }
 
-  private static void checkJetFormat(final TestDB testDB)
-    throws IOException {
+  public void testReadOnlyFormat() throws Exception {
 
-    final FileChannel channel = Database.openChannel(testDB.dbFile, false);
-    try {
+    for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
+
+      Database db = null;
+      IOException failure = null;
+      try {
+        db = openCopy(testDB);
+      } catch(IOException e) {
+        failure = e;
+      } finally {
+        if(db != null) {
+          db.close();
+        }
+      }
 
-      JetFormat fmtActual = JetFormat.getFormat(channel);
-      assertEquals("Unexpected JetFormat for dbFile: " + 
-                   testDB.dbFile.getAbsolutePath(),
-                   testDB.expectedFileFormat.getJetFormat(), fmtActual);
+      if(!testDB.getExpectedFormat().READ_ONLY) {
+        assertNull(failure);
+      } else {
+        assertTrue(failure.getMessage().contains("does not support writing"));
+      }
 
-    } finally {
-      channel.close();
     }
   }
 
-  private static void checkUnsupportedJetFormat(File testDB)
-    throws IOException {
+  public void testFileFormat() throws Exception {
 
-    final FileChannel channel = Database.openChannel(testDB, false);
-    try {
-      JetFormat.getFormat(channel);
-      fail("Unexpected JetFormat for dbFile: " + 
-           testDB.getAbsolutePath());
-    } catch(IOException ignored) {
-      // success
-    } finally {
-      channel.close();
+    for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
+
+      Database db = null;
+      try {
+        db = open(testDB);
+        assertEquals(testDB.getExpectedFileFormat(), db.getFileFormat());
+      } finally {
+        if(db != null) {
+          db.close();
+        }
+      }
     }
   }
 
index a6f3c4426589cfc85c92a3cefbe1468c6eaa9d39..0311088eda5b9abc7e0cac3a4e25c0cbba93aba8 100644 (file)
@@ -45,7 +45,7 @@ public class RelationshipTest extends TestCase {
   }
 
   public void testSimple() throws Exception {
-    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX)) {
+    for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) {
       Database db = open(testDB);
       Table t1 = db.getTable("Table1");
       Table t2 = db.getTable("Table2");
index 54c819adefa36539c57ce39b9036a848ee180071..005d4702a32566495ed358b294807066cf3d3e43 100644 (file)
@@ -29,9 +29,11 @@ package com.healthmarketscience.jackcess;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.TimeZone;
 
 import junit.framework.TestCase;
 
@@ -173,6 +175,14 @@ public class TableTest extends TestCase {
         public Table getTable() {
           return _testTable;
         }
+        @Override
+        protected Charset getCharset() {
+          return getFormat().CHARSET;
+        }
+        @Override
+        protected TimeZone getTimeZone() {
+          return TimeZone.getDefault();
+        }
       };
   }