]> source.dussan.org Git - jackcess.git/commitdiff
Add support for reading/writing complex column data (version history, attachments...
authorJames Ahlborn <jtahlborn@yahoo.com>
Wed, 26 Oct 2011 02:45:20 +0000 (02:45 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Wed, 26 Oct 2011 02:45:20 +0000 (02:45 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@580 f203690c-595d-4dc9-a70b-905162fa7fd2

28 files changed:
TODO.txt
src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/ByteUtil.java
src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/ColumnBuilder.java
src/java/com/healthmarketscience/jackcess/Cursor.java
src/java/com/healthmarketscience/jackcess/DataType.java
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/ExportUtil.java
src/java/com/healthmarketscience/jackcess/IndexData.java
src/java/com/healthmarketscience/jackcess/JetFormat.java
src/java/com/healthmarketscience/jackcess/RowFilter.java
src/java/com/healthmarketscience/jackcess/Table.java
src/java/com/healthmarketscience/jackcess/complex/Attachment.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/ComplexDataType.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/SingleValue.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/Version.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java [new file with mode: 0644]
test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java [new file with mode: 0644]
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java
test/src/java/com/healthmarketscience/jackcess/ExportTest.java
test/src/java/com/healthmarketscience/jackcess/TableTest.java

index 79abf42dce26b8dcd4bd3d283d677f4afacf45e9..3793dbf6d2017bfd583a2a90d280580a4be88302 100644 (file)
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,5 +1,7 @@
 Missing pieces:
 
+- re-use memo/ole data in "other page(s)" when updating rows
+  * MEDIUM
 - fix long text index entries
   * ???
 - implement foreign key index creation
index b045cf8c306ab05203d493fe9dcd402fb0539a03..25bc6bd6b32defee993d2550c96c3f33e90fa9ef 100644 (file)
@@ -4,6 +4,12 @@
     <author email="javajedi@users.sf.net">Tim McCune</author>
   </properties>
   <body>
+    <release version="1.2.6" date="TBD">
+      <action dev="jahlborn" type="update">
+        Add support for reading/writing complex column data (version history,
+        attachments, multi-value columns).
+      </action>
+    </release>
     <release version="1.2.5" date="2011-10-19">
       <action dev="jahlborn" type="update">
         Try multiple classloaders when loading resources as streams.
index c15fc7a792cb35bca7ded7c163a5439d6dff54ef..b39898a0836791ca76a584ce04f13bdac0e1cd87 100644 (file)
@@ -544,7 +544,7 @@ public final class ByteUtil {
   }
 
   /**
-   * Returns a copy of the the given array of the given length.
+   * Returns a copy of the given array of the given length.
    */
   public static byte[] copyOf(byte[] arr, int newLength)
   {
@@ -552,7 +552,7 @@ public final class ByteUtil {
   }
 
   /**
-   * Returns a copy of the the given array of the given length starting at the
+   * Returns a copy of the given array of the given length starting at the
    * given position.
    */
   public static byte[] copyOf(byte[] arr, int offset, int newLength)
index 78dd81b614f47f2be9bcb7c4f2c748f68c814f9b..847fd2bad72800c6ae09464c2de783df03362ca4 100644 (file)
@@ -46,11 +46,15 @@ import java.sql.SQLException;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 import java.util.TimeZone;
 import java.util.UUID;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import com.healthmarketscience.jackcess.complex.ComplexColumnInfo;
+import com.healthmarketscience.jackcess.complex.ComplexValue;
+import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
 import com.healthmarketscience.jackcess.scsu.Compress;
 import com.healthmarketscience.jackcess.scsu.EndOfInputException;
 import com.healthmarketscience.jackcess.scsu.Expand;
@@ -204,6 +208,8 @@ public class Column implements Comparable<Column> {
   private TextInfo _textInfo = DEFAULT_TEXT_INFO;
   /** the auto number generator for this column (if autonumber column) */
   private AutoNumberGenerator _autoNumberGenerator;
+  /** additional information specific to complex columns */
+  private ComplexColumnInfo<? extends ComplexValue> _complexInfo;
   /** properties for this column, if any */
   private PropertyMap _props;  
   
@@ -250,7 +256,7 @@ public class Column implements Comparable<Column> {
     byte colType = buffer.get(offset + getFormat().OFFSET_COLUMN_TYPE);
     _columnNumber = buffer.getShort(offset + getFormat().OFFSET_COLUMN_NUMBER);
     _columnLength = buffer.getShort(offset + getFormat().OFFSET_COLUMN_LENGTH);
-
+    
     byte flags = buffer.get(offset + getFormat().OFFSET_COLUMN_FLAGS);
     _variableLength = ((flags & FIXED_LEN_FLAG_MASK) == 0);
     _autoNumber = ((flags & (AUTO_NUMBER_FLAG_MASK | AUTO_NUMBER_GUID_FLAG_MASK)) != 0);
@@ -291,6 +297,20 @@ public class Column implements Comparable<Column> {
     } else {
       _fixedDataOffset = buffer.getShort(offset + getFormat().OFFSET_COLUMN_FIXED_DATA_OFFSET);
     }
+
+    // load complex info
+    if(_type == DataType.COMPLEX_TYPE) {
+      _complexInfo = ComplexColumnInfo.create(this, buffer, offset);
+    }
+  }
+
+  /**
+   * Secondary column initialization after the table is fully loaded.
+   */
+  void postTableLoadInit() throws IOException {
+    if(_complexInfo != null) {
+      _complexInfo.postTableLoadInit();
+    }
   }
 
   /**
@@ -578,6 +598,40 @@ public class Column implements Comparable<Column> {
     return getDatabase().getTimeZone();
   }
 
+  /**
+   * Whether or not this column is "append only" (its history is tracked by a
+   * separate version history column).
+   * @usage _general_method_
+   */
+  public boolean isAppendOnly() {
+    return (getVersionHistoryColumn() != null);
+  }
+  
+  /**
+   * Returns the column which tracks the version history for an "append only"
+   * column.
+   * @usage _intermediate_method_
+   */
+  public Column getVersionHistoryColumn() {
+    return _textInfo._versionHistoryCol;
+  }
+
+  /**
+   * @usage _advanced_method_
+   */
+  public void setVersionHistoryColumn(Column versionHistoryCol) {
+    modifyTextInfo();
+    _textInfo._versionHistoryCol = versionHistoryCol;
+  }
+  
+  /**
+   * Returns extended functionality for "complex" columns.
+   * @usage _general_method_
+   */
+  public ComplexColumnInfo<? extends ComplexValue> getComplexInfo() {
+    return _complexInfo;
+  }
+  
   private void setUnknownDataType(byte type) {
     // slight hack, stash the original type in the _scale
     modifyNumericInfo();
@@ -609,6 +663,9 @@ public class Column implements Comparable<Column> {
     case GUID:
       _autoNumberGenerator = new GuidAutoNumberGenerator();
       break;
+    case COMPLEX_TYPE:
+      _autoNumberGenerator = new ComplexTypeAutoNumberGenerator();
+      break;
     default:
       LOG.warn("Unknown auto number column type " + _type);
       _autoNumberGenerator = new UnsupportedAutoNumberGenerator(_type);
@@ -664,6 +721,10 @@ public class Column implements Comparable<Column> {
       throw new IllegalArgumentException(
           "Cannot create column with unsupported type " + getType());
     }
+    if(!format.isSupportedDataType(getType())) {
+      throw new IllegalArgumentException(
+          "Database format " + format + " does not support type " + getType());
+    }
     
     if(isVariableLength() != getType().isVariableLength()) {
       throw new IllegalArgumentException("invalid variable length setting");
@@ -711,6 +772,24 @@ public class Column implements Comparable<Column> {
       }
     }
   }
+
+  public Object setRowValue(Object[] rowArray, Object value) {
+    rowArray[_columnIndex] = value;
+    return value;
+  }
+  
+  public Object setRowValue(Map<String,Object> rowMap, Object value) {
+    rowMap.put(_name, value);
+    return value;
+  }
+  
+  public Object getRowValue(Object[] rowArray) {
+    return rowArray[_columnIndex];
+  }
+  
+  public Object getRowValue(Map<String,Object> rowMap) {
+    return rowMap.get(_name);
+  }
   
   /**
    * Deserialize a raw byte value for this column into an Object
@@ -770,6 +849,8 @@ public class Column implements Comparable<Column> {
                (_type == DataType.UNKNOWN_11)) {
       // treat like "binary" data
       return data;
+    } else if (_type == DataType.COMPLEX_TYPE) {
+      return new ComplexValueForeignKey(this, buffer.getInt());
     } else if(_type.isUnsupported()) {
       return rawDataWrapper(data);
     } else {
@@ -1459,6 +1540,9 @@ public class Column implements Comparable<Column> {
     case BINARY:
     case UNKNOWN_0D:
     case UNKNOWN_11:
+    case COMPLEX_TYPE:
+      buffer.putInt(toNumber(obj).intValue());
+      break;
     case UNSUPPORTED_FIXEDLEN:
       byte[] bytes = toByteArray(obj);
       if(bytes.length != getLength()) {
@@ -1658,10 +1742,16 @@ public class Column implements Comparable<Column> {
       if(_textInfo._codePage > 0) {
         rtn.append("\n\tText Code Page: " + _textInfo._codePage);
       }
+      if(isAppendOnly()) {
+        rtn.append("\n\tAppend only: " + isAppendOnly());
+      } 
     }      
     if(_autoNumber) {
       rtn.append("\n\tLast AutoNumber: " + _autoNumberGenerator.getLast());
     }
+    if(_complexInfo != null) {
+      rtn.append("\n\tComplexInfo: " + _complexInfo);
+    }
     rtn.append("\n\n");
     return rtn.toString();
   }
@@ -2091,7 +2181,7 @@ public class Column implements Comparable<Column> {
      * <i>Warning, calling this externally will result in this value being
      * "lost" for the table.</i>
      */
-    public abstract Object getNext();
+    public abstract Object getNext(Object prevRowValue);
 
     /**
      * Returns the flags used when writing this column.
@@ -2115,7 +2205,7 @@ public class Column implements Comparable<Column> {
     }
 
     @Override
-    public Object getNext() {
+    public Object getNext(Object prevRowValue) {
       // the table stores the last long autonumber used
       return getTable().getNextLongAutoNumber();
     }
@@ -2143,7 +2233,7 @@ public class Column implements Comparable<Column> {
     }
 
     @Override
-    public Object getNext() {
+    public Object getNext(Object prevRowValue) {
       // format guids consistently w/ Column.readGUIDValue()
       _lastAutoNumber = "{" + UUID.randomUUID() + "}";
       return _lastAutoNumber;
@@ -2160,6 +2250,38 @@ public class Column implements Comparable<Column> {
     }
   }
 
+  private final class ComplexTypeAutoNumberGenerator extends AutoNumberGenerator
+  {
+    private ComplexTypeAutoNumberGenerator() {}
+
+    @Override
+    public Object getLast() {
+      // the table stores the last ComplexType autonumber used
+      return getTable().getLastComplexTypeAutoNumber();
+    }
+
+    @Override
+    public Object getNext(Object prevRowValue) {
+      int nextComplexAutoNum =
+        ((prevRowValue == null) ?
+         // the table stores the last ComplexType autonumber used
+         getTable().getNextComplexTypeAutoNumber() :
+         // same value is shared across all ComplexType values in a row
+         ((ComplexValueForeignKey)prevRowValue).get());
+      return new ComplexValueForeignKey(Column.this, nextComplexAutoNum);
+    }
+
+    @Override
+    public int getColumnFlags() {
+      return AUTO_NUMBER_FLAG_MASK;
+    }
+
+    @Override
+    public DataType getType() {
+      return DataType.COMPLEX_TYPE;
+    }
+  }
+  
   private final class UnsupportedAutoNumberGenerator extends AutoNumberGenerator
   {
     private final DataType _genType;
@@ -2174,7 +2296,7 @@ public class Column implements Comparable<Column> {
     }
 
     @Override
-    public Object getNext() {
+    public Object getNext(Object prevRowValue) {
       throw new UnsupportedOperationException();
     }
 
@@ -2253,5 +2375,8 @@ public class Column implements Comparable<Column> {
     private SortOrder _sortOrder;
     /** the code page for a text field (for certain db versions) */
     private short _codePage;
+    /** complex column which tracks the version history for this "append only"
+        column */
+    private Column _versionHistoryCol;
   }
 }
index 06e7356f1c75c5e132b6f435ed44dfb3e1f53477..befff676371718828e96b9bf438f3ecab1bb4c62 100644 (file)
@@ -151,6 +151,7 @@ public class ColumnBuilder {
       setScale(template.getScale());
       setPrecision(template.getPrecision());
     }
+    setCompressedUnicode(template.isCompressedUnicode());
     
     return this;
   }
index 045fcea165446e351ed9b18f34caa05b025a81fe..623bebd5351591707fd359fc897c99fd5fe6bb51 100644 (file)
@@ -927,7 +927,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
   {
     Object[] row = new Object[_table.getColumnCount()];
     Arrays.fill(row, Column.KEEP_VALUE);
-    row[column.getColumnIndex()] = value;
+    column.setRowValue(row, value);
     _table.updateRow(_rowState, _curPos.getRowId(), row);
   }
 
index bc5e13d0d8f4115d29a79820708d840125877eb5..8d20c1df54ef5cab4af5c9e0a2b337ec9a505936 100644 (file)
@@ -147,6 +147,11 @@ public enum DataType {
    * MSysAccessObjects table).  Handled like a fixed length BINARY/OLE.
    */
   UNKNOWN_11((byte) 0x11, null, 3992),
+  /**
+   * Complex type corresponds to a special LONG autonumber field which is the
+   * key for a secondary table which holds the "real" data.
+   */
+  COMPLEX_TYPE((byte) 0x12, null, 4),    
   /**
    * Dummy type for a fixed length type which is not currently supported.
    * Handled like a fixed length BINARY.
@@ -410,7 +415,11 @@ public enum DataType {
   }
 
   public boolean mayBeAutoNumber() {
-    return((this == LONG) || (this == GUID));
+    return((this == LONG) || (this == GUID) || (this == COMPLEX_TYPE));
+  }
+
+  public boolean isMultipleAutoNumberAllowed() {
+    return (this == COMPLEX_TYPE);
   }
 
   public boolean isUnsupported() {
index 549be7fb7ce4a0e81af7c406bc00325cff94ff2a..4eb0b9b388ca9d04dc368cc7537b1ce751ed2279 100644 (file)
@@ -1253,6 +1253,13 @@ public class Database
     Set<String> colNames = new HashSet<String>();
     // next, validate the column definitions
     for(Column column : columns) {
+
+      // FIXME for now, we can't create complex columns
+      if(column.getType() == DataType.COMPLEX_TYPE) {
+        throw new UnsupportedOperationException(
+            "Complex column creation is not yet implemented");
+      }
+      
       column.validate(_format);
       if(!colNames.add(column.getName().toUpperCase())) {
         throw new IllegalArgumentException("duplicate column name: " +
@@ -1267,12 +1274,14 @@ public class Database
 
     List<Column> autoCols = Table.getAutoNumberColumns(columns);
     if(autoCols.size() > 1) {
-      // we can have one of each type
+      // for most autonumber types, we can only have one of each type
       Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
       for(Column c : autoCols) {
-        if(!autoTypes.add(c.getType())) {
+        if(!c.getType().isMultipleAutoNumberAllowed() &&
+           !autoTypes.add(c.getType())) {
           throw new IllegalArgumentException(
-              "Can have at most one AutoNumber column of type " + c.getType() + " per table");
+              "Can have at most one AutoNumber column of type " + c.getType() +
+              " per table");
         }
       }
     }
@@ -1661,10 +1670,10 @@ public class Database
     List<Object[]> aceRows = new ArrayList<Object[]>(_newTableSIDs.size());
     for(byte[] sid : _newTableSIDs) {
       Object[] aceRow = new Object[acEntries.getColumnCount()];
-      aceRow[acmCol.getColumnIndex()] = SYS_FULL_ACCESS_ACM;
-      aceRow[inheritCol.getColumnIndex()] = Boolean.FALSE;
-      aceRow[objIdCol.getColumnIndex()] = Integer.valueOf(pageNumber);
-      aceRow[sidCol.getColumnIndex()] = sid;
+      acmCol.setRowValue(aceRow, SYS_FULL_ACCESS_ACM);
+      inheritCol.setRowValue(aceRow, Boolean.FALSE);
+      objIdCol.setRowValue(aceRow, Integer.valueOf(pageNumber));
+      sidCol.setRowValue(aceRow, sid);
       aceRows.add(aceRow);
     }
     acEntries.addRows(aceRows);  
index 6c903fb5fee4123d12a47aada18d6c2310a3ec57..7876fb09739cc1a7e9afcf6fe0deabb18f5906f3 100644 (file)
@@ -327,7 +327,7 @@ public class ExportUtil {
 
       // fill raw row data in array
       for (int i = 0; i < columns.size(); i++) {
-        unfilteredRowData[i] = row.get(columns.get(i).getName());
+        unfilteredRowData[i] = columns.get(i).getRowValue(row);
       }
 
       // apply filter
index 11b7b0a35098eed1bd8d6922dab2cc90e266bafa..b9793156fdce61f09fa660464bb0130515870294 100644 (file)
@@ -1205,6 +1205,7 @@ public abstract class IndexData {
     case INT:
     case LONG:
     case MONEY:
+    case COMPLEX_TYPE:
       return new IntegerColumnDescriptor(col, flags);
     case FLOAT:
     case DOUBLE:
index cfc5d974632cdad95eef6692b0fb70ba1addfe7c..bd33fdc874c8522c7c6af3d043bf74db83ed89d7 100644 (file)
@@ -182,6 +182,7 @@ public abstract class JetFormat {
   public final int OFFSET_NEXT_TABLE_DEF_PAGE;
   public final int OFFSET_NUM_ROWS;
   public final int OFFSET_NEXT_AUTO_NUMBER;
+  public final int OFFSET_NEXT_COMPLEX_AUTO_NUMBER;
   public final int OFFSET_TABLE_TYPE;
   public final int OFFSET_MAX_COLS;
   public final int OFFSET_NUM_VAR_COLS;
@@ -201,6 +202,7 @@ public abstract class JetFormat {
   public final int OFFSET_COLUMN_SCALE;
   public final int OFFSET_COLUMN_SORT_ORDER;
   public final int OFFSET_COLUMN_CODE_PAGE;
+  public final int OFFSET_COLUMN_COMPLEX_ID;
   public final int OFFSET_COLUMN_FLAGS;
   public final int OFFSET_COLUMN_COMPRESSED_UNICODE;
   public final int OFFSET_COLUMN_LENGTH;
@@ -313,6 +315,7 @@ public abstract class JetFormat {
     OFFSET_NEXT_TABLE_DEF_PAGE = defineOffsetNextTableDefPage();
     OFFSET_NUM_ROWS = defineOffsetNumRows();
     OFFSET_NEXT_AUTO_NUMBER = defineOffsetNextAutoNumber();
+    OFFSET_NEXT_COMPLEX_AUTO_NUMBER = defineOffsetNextComplexAutoNumber();
     OFFSET_TABLE_TYPE = defineOffsetTableType();
     OFFSET_MAX_COLS = defineOffsetMaxCols();
     OFFSET_NUM_VAR_COLS = defineOffsetNumVarCols();
@@ -332,6 +335,7 @@ public abstract class JetFormat {
     OFFSET_COLUMN_SCALE = defineOffsetColumnScale();
     OFFSET_COLUMN_SORT_ORDER = defineOffsetColumnSortOrder();
     OFFSET_COLUMN_CODE_PAGE = defineOffsetColumnCodePage();
+    OFFSET_COLUMN_COMPLEX_ID = defineOffsetColumnComplexId();
     OFFSET_COLUMN_FLAGS = defineOffsetColumnFlags();
     OFFSET_COLUMN_COMPRESSED_UNICODE = defineOffsetColumnCompressedUnicode();
     OFFSET_COLUMN_LENGTH = defineOffsetColumnLength();
@@ -412,6 +416,7 @@ public abstract class JetFormat {
   protected abstract int defineOffsetNextTableDefPage();
   protected abstract int defineOffsetNumRows();
   protected abstract int defineOffsetNextAutoNumber();
+  protected abstract int defineOffsetNextComplexAutoNumber();
   protected abstract int defineOffsetTableType();
   protected abstract int defineOffsetMaxCols();
   protected abstract int defineOffsetNumVarCols();
@@ -431,6 +436,7 @@ public abstract class JetFormat {
   protected abstract int defineOffsetColumnScale();
   protected abstract int defineOffsetColumnSortOrder();
   protected abstract int defineOffsetColumnCodePage();
+  protected abstract int defineOffsetColumnComplexId();
   protected abstract int defineOffsetColumnFlags();
   protected abstract int defineOffsetColumnCompressedUnicode();
   protected abstract int defineOffsetColumnLength();
@@ -490,6 +496,8 @@ public abstract class JetFormat {
 
   protected abstract Map<String,Database.FileFormat> getPossibleFileFormats();
 
+  protected abstract boolean isSupportedDataType(DataType type);
+
   @Override
   public String toString() {
     return _name;
@@ -552,6 +560,8 @@ public abstract class JetFormat {
     @Override
     protected int defineOffsetNextAutoNumber() { return 20; }
     @Override
+    protected int defineOffsetNextComplexAutoNumber() { return -1; }
+    @Override
     protected int defineOffsetTableType() { return 20; }
     @Override
     protected int defineOffsetMaxCols() { return 21; }
@@ -588,6 +598,8 @@ public abstract class JetFormat {
     @Override
     protected int defineOffsetColumnCodePage() { return 11; }
     @Override
+    protected int defineOffsetColumnComplexId() { return -1; }
+    @Override
     protected int defineOffsetColumnFlags() { return 13; }
     @Override
     protected int defineOffsetColumnCompressedUnicode() { return 16; }
@@ -701,6 +713,10 @@ public abstract class JetFormat {
       return PossibleFileFormats.POSSIBLE_VERSION_3;
     }
 
+    @Override
+    protected boolean isSupportedDataType(DataType type) {
+      return (type != DataType.COMPLEX_TYPE);
+    }
   }
   
   private static class Jet4Format extends JetFormat {
@@ -762,6 +778,8 @@ public abstract class JetFormat {
     @Override
     protected int defineOffsetNextAutoNumber() { return 20; }
     @Override
+    protected int defineOffsetNextComplexAutoNumber() { return -1; }
+    @Override
     protected int defineOffsetTableType() { return 40; }
     @Override
     protected int defineOffsetMaxCols() { return 41; }
@@ -798,6 +816,8 @@ public abstract class JetFormat {
     @Override
     protected int defineOffsetColumnCodePage() { return -1; }
     @Override
+    protected int defineOffsetColumnComplexId() { return -1; }
+    @Override
     protected int defineOffsetColumnFlags() { return 15; }
     @Override
     protected int defineOffsetColumnCompressedUnicode() { return 16; }
@@ -911,6 +931,10 @@ public abstract class JetFormat {
       return PossibleFileFormats.POSSIBLE_VERSION_4;
     }
 
+    @Override
+    protected boolean isSupportedDataType(DataType type) {
+      return (type != DataType.COMPLEX_TYPE);
+    }
   }
   
   private static final class MSISAMFormat extends Jet4Format {
@@ -947,6 +971,17 @@ public abstract class JetFormat {
     protected Map<String,Database.FileFormat> getPossibleFileFormats() {
       return PossibleFileFormats.POSSIBLE_VERSION_12;
     }
+
+    @Override
+    protected int defineOffsetNextComplexAutoNumber() { return 28; }
+
+    @Override
+    protected int defineOffsetColumnComplexId() { return 11; }
+    
+    @Override
+    protected boolean isSupportedDataType(DataType type) {
+      return true;
+    }
   }
 
   private static final class Jet14Format extends Jet12Format {
index 4f519e880dada187bed87ec0f2503c426d65dcd1..3a537af8efe77325a0fef841483caaa38742a5ef 100644 (file)
@@ -107,7 +107,7 @@ public abstract class RowFilter
         @Override
         public boolean matches(Map<String, Object> row) 
         {
-          return ObjectUtils.equals(valuePattern, row.get(columnPattern.getName()));
+          return ObjectUtils.equals(valuePattern, columnPattern.getRowValue(row));
         }
       };
   }
index 30a48c780894be4048aa95a3879c2b68e29388dc..0cf32ea52f2f490ac50fb12834c7e2be58640ffb 100644 (file)
@@ -126,6 +126,8 @@ public class Table
   private int _rowCount;
   /** last long auto number for the table */
   private int _lastLongAutoNumber;
+  /** last complex type auto number for the table */
+  private int _lastComplexTypeAutoNumber;
   /** page number of the definition of this table */
   private final int _tableDefPageNumber;
   /** max Number of columns in the table (includes previous deletions) */
@@ -136,6 +138,8 @@ public class Table
   private List<Column> _columns = new ArrayList<Column>();
   /** List of variable length columns in this table, ordered by offset */
   private List<Column> _varColumns = new ArrayList<Column>();
+  /** List of autonumber columns in this table, ordered by column number */
+  private List<Column> _autoNumColumns;
   /** List of indexes on this table (multiple logical indexes may be backed by
       the same index data) */
   private List<Index> _indexes = new ArrayList<Index>();
@@ -307,7 +311,7 @@ public class Table
     _tableErrorHandler = newErrorHandler;
   }    
 
-  protected int getTableDefPageNumber() {
+  public int getTableDefPageNumber() {
     return _tableDefPageNumber;
   }
 
@@ -393,6 +397,7 @@ public class Table
     }
     _maxColumnCount = (short)_columns.size();
     _maxVarColumnCount = (short)_varColumns.size();
+    _autoNumColumns = getAutoNumberColumns(columns);
   }
 
   /**
@@ -635,8 +640,8 @@ public class Table
 
       if((columnNames == null) || (columnNames.contains(column.getName()))) {
         // Add the value to the row data
-        rtn.put(column.getName(), 
-                getRowColumn(format, rowBuffer, nullMask, column, rowState));
+        column.setRowValue(
+            rtn, getRowColumn(format, rowBuffer, nullMask, column, rowState));
       }
     }
     return rtn;
@@ -1222,6 +1227,10 @@ public class Table
     }
     _rowCount = tableBuffer.getInt(getFormat().OFFSET_NUM_ROWS);
     _lastLongAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER);
+    if(getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER >= 0) {
+      _lastComplexTypeAutoNumber = tableBuffer.getInt(
+          getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER);
+    }
     _tableType = tableBuffer.get(getFormat().OFFSET_TABLE_TYPE);
     _maxColumnCount = tableBuffer.getShort(getFormat().OFFSET_MAX_COLS);
     _maxVarColumnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_VAR_COLS);
@@ -1262,6 +1271,7 @@ public class Table
       column.setName(readName(tableBuffer));
     }    
     Collections.sort(_columns);
+    _autoNumColumns = getAutoNumberColumns(_columns);
 
     // setup the data index for the columns
     int colIdx = 0;
@@ -1294,6 +1304,12 @@ public class Table
     if(getDatabase().getColumnOrder() != ColumnOrder.DATA) {
       Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR);
     }
+
+    for(Column col : _columns) {
+      // some columns need to do extra work after the table is completely
+      // loaded
+      col.postTableLoadInit();
+    }
   }
   
   /**
@@ -1367,7 +1383,7 @@ public class Table
     }
     for(Column col : _columns) {
       if(rowMap.containsKey(col.getName())) {
-        row[col.getColumnIndex()] = rowMap.get(col.getName());
+        col.setRowValue(row, col.getRowValue(rowMap));
       }
     }
     return row;
@@ -1436,10 +1452,13 @@ public class Table
         rows.set(i, row);
       }
 
+      // fill in autonumbers
+      handleAutoNumbersForAdd(row);
+      
       // write the row of data to a temporary buffer
       rowData[i] = createRow(row, getFormat().MAX_ROW_SIZE,
                              writeRowBufferH.getPageBuffer(getPageChannel()),
-                             false, 0);
+                             0);
       
       if (rowData[i].limit() > getFormat().MAX_ROW_SIZE) {
         throw new IOException("Row size " + rowData[i].limit() +
@@ -1512,21 +1531,25 @@ public class Table
       row = dupeRow(row, _columns.size());
     }
 
-    // fill in any auto-numbers (we don't allow autonumber values to be
-    // modified) or "keep value" fields
     NullMask nullMask = getRowNullMask(rowBuffer);
+
+    // fill in any auto-numbers (we don't allow autonumber values to be
+    // modified)
+    handleAutoNumbersForUpdate(row, rowBuffer, nullMask, rowState);
+    
+    // fill in any "keep value" fields
     for(Column column : _columns) {
-      if(column.isAutoNumber() || 
-         (row[column.getColumnIndex()] == Column.KEEP_VALUE)) {
-        row[column.getColumnIndex()] = getRowColumn(getFormat(), rowBuffer, nullMask,
-                                                    column, rowState);
+      if(column.getRowValue(row) == Column.KEEP_VALUE) {
+        column.setRowValue(
+            row, getRowColumn(
+                getFormat(), rowBuffer, nullMask, column, rowState));
       }
     }
 
     // generate new row bytes
     ByteBuffer newRowData = createRow(
         row, getFormat().MAX_ROW_SIZE,
-        _singleRowBufferH.getPageBuffer(getPageChannel()), true, oldRowSize);
+        _singleRowBufferH.getPageBuffer(getPageChannel()), oldRowSize);
 
     if (newRowData.limit() > getFormat().MAX_ROW_SIZE) {
       throw new IOException("Row size " + newRowData.limit() + 
@@ -1666,6 +1689,10 @@ public class Table
     _rowCount += rowCountInc;
     tdefPage.putInt(getFormat().OFFSET_NUM_ROWS, _rowCount);
     tdefPage.putInt(getFormat().OFFSET_NEXT_AUTO_NUMBER, _lastLongAutoNumber);
+    int ctypeOff = getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER;
+    if(ctypeOff >= 0) {
+      tdefPage.putInt(ctypeOff, _lastComplexTypeAutoNumber);
+    }
 
     // write any index changes
     for (IndexData indexData : _indexDatas) {
@@ -1715,7 +1742,7 @@ public class Table
    * @return the given buffer, filled with the row data
    */
   ByteBuffer createRow(Object[] rowArray, int maxRowSize, ByteBuffer buffer,
-                       boolean isUpdate, int minRowSize)
+                       int minRowSize)
     throws IOException
   {
     buffer.putShort(_maxColumnCount);
@@ -1730,7 +1757,7 @@ public class Table
         continue;
       }
         
-      Object rowValue = rowArray[col.getColumnIndex()];
+      Object rowValue = col.getRowValue(rowArray);
 
       if (col.getType() == DataType.BOOLEAN) {
         
@@ -1739,15 +1766,6 @@ public class Table
           nullMask.markNotNull(col);
         }
         rowValue = null;
-        
-      } else if(col.isAutoNumber() && !isUpdate) {
-            
-        // ignore given row value, use next autonumber
-        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)
-        rowArray[col.getColumnIndex()] = rowValue;
       }
           
       if(rowValue != null) {
@@ -1758,7 +1776,6 @@ public class Table
         // remainingRowLength is ignored when writing fixed length data
         buffer.position(fixedDataStart + col.getFixedDataOffset());
         buffer.put(col.write(rowValue, 0));
-
       }
 
       // always insert space for the entire fixed data column length
@@ -1792,7 +1809,7 @@ public class Table
       // later by being too greedy
       for (Column varCol : _varColumns) {
         if((varCol.getType().isLongValue()) &&
-           (rowArray[varCol.getColumnIndex()] != null)) {
+           (varCol.getRowValue(rowArray) != null)) {
           maxRowSize -= getFormat().SIZE_LONG_VALUE_DEF;
         }
       }
@@ -1802,7 +1819,7 @@ public class Table
       int varColumnOffsetsIndex = 0;
       for (Column varCol : _varColumns) {
         short offset = (short) buffer.position();
-        Object rowValue = rowArray[varCol.getColumnIndex()];
+        Object rowValue = varCol.getRowValue(rowArray);
         if (rowValue != null) {
           // we have a value
           nullMask.markNotNull(varCol);
@@ -1857,6 +1874,50 @@ public class Table
     return buffer;
   }
 
+  /**
+   * Autonumber columns may not be modified on update.
+   */
+  private void handleAutoNumbersForUpdate(
+      Object[] row, ByteBuffer rowBuffer, NullMask nullMask, RowState rowState)
+    throws IOException
+  {
+    if(_autoNumColumns.isEmpty()) {
+      return;
+    }
+
+    for(Column col : _autoNumColumns) {
+      col.setRowValue(
+          row, getRowColumn(getFormat(), rowBuffer, nullMask, col, rowState));
+    }
+  }
+
+  /**
+   * Fill in all autonumber column values.
+   */
+  private void handleAutoNumbersForAdd(Object[] row)
+    throws IOException
+  {
+    if(_autoNumColumns.isEmpty()) {
+      return;
+    }
+
+    Object complexAutoNumber = null;
+    for(Column col : _autoNumColumns) {
+      // ignore given row value, use next autonumber
+      Column.AutoNumberGenerator autoNumGen = col.getAutoNumberGenerator();
+      Object rowValue = null;
+      if(autoNumGen.getType() != DataType.COMPLEX_TYPE) {
+        rowValue = autoNumGen.getNext(null);
+      } else {
+        // complex type auto numbers are shared across all complex columns
+        // in the row
+        complexAutoNumber = autoNumGen.getNext(complexAutoNumber);
+        rowValue = complexAutoNumber;
+      }
+      col.setRowValue(row, rowValue);
+    }
+  }
+
   private static void padRowBuffer(ByteBuffer buffer, int minRowSize,
                                    int trailerSize)
   {
@@ -1886,6 +1947,16 @@ public class Table
     return _lastLongAutoNumber;
   }
   
+  int getNextComplexTypeAutoNumber() {
+    // note, the saved value is the last one handed out, so pre-increment
+    return ++_lastComplexTypeAutoNumber;
+  }
+
+  int getLastComplexTypeAutoNumber() {
+    // gets the last used auto number (does not modify)
+    return _lastComplexTypeAutoNumber;
+  }
+  
   @Override
   public String toString() {
     StringBuilder rtn = new StringBuilder();
@@ -2105,7 +2176,7 @@ public class Table
    * @usage _advanced_method_
    */
   public static List<Column> getAutoNumberColumns(Collection<Column> columns) {
-    List<Column> autoCols = new ArrayList<Column>();
+    List<Column> autoCols = new ArrayList<Column>(1);
     for(Column c : columns) {
       if(c.isAutoNumber()) {
         autoCols.add(c);
diff --git a/src/java/com/healthmarketscience/jackcess/complex/Attachment.java b/src/java/com/healthmarketscience/jackcess/complex/Attachment.java
new file mode 100644 (file)
index 0000000..af33544
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.util.Date;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public interface Attachment extends ComplexValue 
+{
+  public byte[] getFileData();
+
+  public void setFileData(byte[] data);
+
+  public String getFileName();
+
+  public void setFileName(String fileName);
+  
+  public String getFileUrl();
+
+  public void setFileUrl(String fileUrl);
+  
+  public String getFileType();
+
+  public void setFileType(String fileType);
+  
+  public Date getFileTimeStamp();
+
+  public void setFileTimeStamp(Date fileTimeStamp);
+  
+  public Integer getFileFlags();
+
+  public void setFileFlags(Integer fileFlags);  
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java
new file mode 100644 (file)
index 0000000..f258ca0
--- /dev/null
@@ -0,0 +1,326 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import com.healthmarketscience.jackcess.ByteUtil;
+import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.Table;
+
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
+{
+  private static final String FILE_NAME_COL_NAME = "FileName";
+  private static final String FILE_TYPE_COL_NAME = "FileType";
+
+  private final Column _fileUrlCol;
+  private final Column _fileNameCol;
+  private final Column _fileTypeCol;
+  private final Column _fileDataCol;
+  private final Column _fileTimeStampCol;
+  private final Column _fileFlagsCol;
+  
+  public AttachmentColumnInfo(Column column, int complexId,
+                              Table typeObjTable, Table flatTable)
+    throws IOException
+  {
+    super(column, complexId, typeObjTable, flatTable);
+
+    Column fileUrlCol = null;
+    Column fileNameCol = null;
+    Column fileTypeCol = null;
+    Column fileDataCol = null;
+    Column fileTimeStampCol = null;
+    Column fileFlagsCol = null;
+
+    for(Column col : getTypeColumns()) {
+      switch(col.getType()) {
+      case TEXT:
+        if(FILE_NAME_COL_NAME.equalsIgnoreCase(col.getName())) {
+          fileNameCol = col;
+        } else if(FILE_TYPE_COL_NAME.equalsIgnoreCase(col.getName())) {
+          fileTypeCol = col;
+        } else {
+          // if names don't match, assign in order: name, type
+          if(fileNameCol == null) {
+            fileNameCol = col;
+          } else if(fileTypeCol == null) {
+            fileTypeCol = col;
+          }
+        }
+        break;
+      case LONG:
+        fileFlagsCol = col;
+        break;
+      case SHORT_DATE_TIME:
+        fileTimeStampCol = col;
+        break;
+      case OLE:
+        fileDataCol = col;
+        break;
+      case MEMO:
+        fileUrlCol = col;
+        break;
+      default:
+        // ignore
+      }
+    }
+    
+    _fileUrlCol = fileUrlCol;
+    _fileNameCol = fileNameCol;
+    _fileTypeCol = fileTypeCol;
+    _fileDataCol = fileDataCol;
+    _fileTimeStampCol = fileTimeStampCol;
+    _fileFlagsCol = fileFlagsCol;
+  }
+
+  public Column getFileUrlColumn() {
+    return _fileUrlCol;
+  }
+  
+  public Column getFileNameColumn() {
+    return _fileNameCol;
+  }
+
+  public Column getFileTypeColumn() {
+    return _fileTypeCol;
+  }
+  
+  public Column getFileDataColumn() {
+    return _fileDataCol;
+  }
+  
+  public Column getFileTimeStampColumn() {
+    return _fileTimeStampCol;
+  }
+  
+  public Column getFileFlagsColumn() {
+    return _fileFlagsCol;
+  }  
+  
+  @Override
+  public ComplexDataType getType()
+  {
+    return ComplexDataType.ATTACHMENT;
+  }
+
+  @Override
+  protected List<Attachment> toValues(ComplexValueForeignKey complexValueFk,
+                                   List<Map<String,Object>> rawValues)
+    throws IOException
+  {
+    List<Attachment> attachments = new ArrayList<Attachment>();
+    for(Map<String,Object> rawValue : rawValues) {
+      attachments.add(toAttachment(complexValueFk, rawValue));
+    }
+    return attachments;
+  }
+
+  protected AttachmentImpl toAttachment(ComplexValueForeignKey complexValueFk,
+                                        Map<String,Object> rawValue) {
+    int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue);
+    String url = (String)getFileUrlColumn().getRowValue(rawValue);
+    String name = (String)getFileNameColumn().getRowValue(rawValue);
+    String type = (String)getFileTypeColumn().getRowValue(rawValue);
+    Integer flags = (Integer)getFileFlagsColumn().getRowValue(rawValue);
+    Date ts = (Date)getFileTimeStampColumn().getRowValue(rawValue);
+    byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue);
+    
+    return new AttachmentImpl(id, complexValueFk, url, name, type, data,
+                              ts, flags);
+  }
+
+  @Override
+  protected Object[] asRow(Object[] row, Attachment attachment) {
+    super.asRow(row, attachment);
+    getFileUrlColumn().setRowValue(row, attachment.getFileUrl());
+    getFileNameColumn().setRowValue(row, attachment.getFileName());
+    getFileTypeColumn().setRowValue(row, attachment.getFileType());
+    getFileFlagsColumn().setRowValue(row, attachment.getFileFlags());
+    getFileTimeStampColumn().setRowValue(row, attachment.getFileTimeStamp());
+    getFileDataColumn().setRowValue(row, attachment.getFileData());
+    return row;
+  }
+
+  public static Attachment newAttachment(byte[] data) {
+    return newAttachment(INVALID_COMPLEX_VALUE_ID, data);
+  }
+  
+  public static Attachment newAttachment(ComplexValueForeignKey complexValueFk,
+                                         byte[] data) {
+    return newAttachment(complexValueFk, null, null, null, data, null, null);
+  }
+
+  public static Attachment newAttachment(
+      String url, String name, String type, byte[] data,
+      Date timeStamp, Integer flags)
+  {
+    return newAttachment(INVALID_COMPLEX_VALUE_ID, url, name, type, data,
+                         timeStamp, flags);
+  }
+  
+  public static Attachment newAttachment(
+      ComplexValueForeignKey complexValueFk, String url, String name,
+      String type, byte[] data, Date timeStamp, Integer flags)
+  {
+    return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type,
+                              data, timeStamp, flags);
+  }
+
+  
+  public static boolean isAttachmentColumn(Table typeObjTable) {
+    // attachment data has these columns FileURL(MEMO), FileName(TEXT),
+    // FileType(TEXT), FileData(OLE), FileTimeStamp(SHORT_DATE_TIME),
+    // FileFlags(LONG)
+    List<Column> typeCols = typeObjTable.getColumns();
+    if(typeCols.size() < 6) {
+      return false;
+    }
+
+    int numMemo = 0;
+    int numText = 0;
+    int numDate = 0;
+    int numOle= 0;
+    int numLong = 0;
+    
+    for(Column col : typeCols) {
+      switch(col.getType()) {
+      case TEXT:
+        ++numText;
+        break;
+      case LONG:
+        ++numLong;
+        break;
+      case SHORT_DATE_TIME:
+        ++numDate;
+        break;
+      case OLE:
+        ++numOle;
+        break;
+      case MEMO:
+        ++numMemo;
+        break;
+      default:
+        // ignore
+      }
+    }
+
+    // be flexible, allow for extra columns...
+    return((numMemo >= 1) && (numText >= 2) && (numOle >= 1) &&
+           (numDate >= 1) && (numLong >= 1));
+  }
+
+
+  private static class AttachmentImpl extends ComplexValueImpl
+    implements Attachment
+  {
+    private String _url;
+    private String _name;
+    private String _type;
+    private byte[] _data;
+    private Date _timeStamp;
+    private Integer _flags;
+
+    private AttachmentImpl(int id, ComplexValueForeignKey complexValueFk,
+                           String url, String name, String type, byte[] data,
+                           Date timeStamp, Integer flags)
+    {
+      super(id, complexValueFk);
+      _url = url;
+      _name = name;
+      _type = type;
+      _data = data;
+      _timeStamp = timeStamp;
+      _flags = flags;
+    }
+    
+    public byte[] getFileData() {
+      return _data;
+    }
+
+    public void setFileData(byte[] data) {
+      _data = data;
+    }
+
+    public String getFileName() {
+      return _name;
+    }
+
+    public void setFileName(String fileName) {
+      _name = fileName;
+    }
+  
+    public String getFileUrl() {
+      return _url;
+    }
+
+    public void setFileUrl(String fileUrl) {
+      _url = fileUrl;
+    }
+  
+    public String getFileType() {
+      return _type;
+    }
+
+    public void setFileType(String fileType) {
+      _type = fileType;
+    }
+  
+    public Date getFileTimeStamp() {
+      return _timeStamp;
+    }
+
+    public void setFileTimeStamp(Date fileTimeStamp) {
+      _timeStamp = fileTimeStamp;
+    }
+  
+    public Integer getFileFlags() {
+      return _flags;
+    }
+
+    public void setFileFlags(Integer fileFlags) {
+      _flags = fileFlags;
+    }  
+
+    @Override
+    public void update() throws IOException {
+      getComplexValueForeignKey().updateAttachment(this);
+    }
+    
+    @Override
+    public String toString()
+    {
+      return "Attachment(" + getComplexValueForeignKey() + "," + getId() +
+        ") " + getFileUrl() + ", " + getFileName() + ", " + getFileType()
+        + ", " + getFileTimeStamp() + ", " + getFileFlags()  + ", " +
+        ByteUtil.toHexString(getFileData());
+    } 
+  }
+  
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java
new file mode 100644 (file)
index 0000000..5232637
--- /dev/null
@@ -0,0 +1,400 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.CursorBuilder;
+import com.healthmarketscience.jackcess.DataType;
+import com.healthmarketscience.jackcess.Database;
+import com.healthmarketscience.jackcess.IndexCursor;
+import com.healthmarketscience.jackcess.JetFormat;
+import com.healthmarketscience.jackcess.PageChannel;
+import com.healthmarketscience.jackcess.Table;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Base class for the additional information tracked for complex columns.
+ *
+ * @author James Ahlborn
+ */
+public abstract class ComplexColumnInfo<V extends ComplexValue>
+{
+  private static final Log LOG = LogFactory.getLog(Column.class);
+
+  public static final int INVALID_ID = -1;
+  public static final ComplexValueForeignKey INVALID_COMPLEX_VALUE_ID =
+    new ComplexValueForeignKey(null, INVALID_ID);
+  
+  private static final String COL_COMPLEX_TYPE_OBJECT_ID = "ComplexTypeObjectID";
+  private static final String COL_TABLE_ID = "ConceptualTableID";
+  private static final String COL_FLAT_TABLE_ID = "FlatTableID";
+
+  private final Column _column;
+  private final int _complexTypeId;
+  private final Table _flatTable;
+  private final List<Column> _typeCols;
+  private final Column _pkCol;
+  private final Column _complexValFkCol;
+  private IndexCursor _pkCursor;
+  private IndexCursor _complexValIdCursor;
+  
+  protected ComplexColumnInfo(Column column, int complexTypeId,
+                              Table typeObjTable, Table flatTable)
+    throws IOException
+  {
+    _column = column;
+    _complexTypeId = complexTypeId;
+    _flatTable = flatTable;
+    
+    // the flat table has all the "value" columns and 2 extra columns, a
+    // primary key for each row, and a LONG value which is essentially a
+    // foreign key to the main table.
+    _typeCols = new ArrayList<Column>();
+    List<Column> otherCols = new ArrayList<Column>();
+    diffFlatColumns(typeObjTable, flatTable, _typeCols, otherCols);
+
+    Column pkCol = null;
+    Column complexValFkCol = null;
+    for(Column col : otherCols) {
+      if(col.isAutoNumber()) {
+        pkCol = col;
+      } else if(col.getType() == DataType.LONG) {
+        complexValFkCol = col;
+      }
+    }
+
+    if((pkCol == null) || (complexValFkCol == null)) {
+      throw new IOException("Could not find expected columns in flat table " +
+                            flatTable.getName() + " for complex column with id "
+                            + complexTypeId);
+    }
+    _pkCol = pkCol;
+    _complexValFkCol = complexValFkCol;
+  }
+
+  public static ComplexColumnInfo<? extends ComplexValue> create(
+      Column column, ByteBuffer buffer, int offset)
+    throws IOException
+  {
+    int complexTypeId = buffer.getInt(
+        offset + column.getFormat().OFFSET_COLUMN_COMPLEX_ID);
+
+    Database db = column.getDatabase();
+    Table complexColumns = db.getSystemComplexColumns();
+    IndexCursor cursor = IndexCursor.createCursor(
+        complexColumns, complexColumns.getPrimaryKeyIndex());
+    if(!cursor.findRowByEntry(complexTypeId)) {
+      throw new IOException(
+          "Could not find complex column info for complex column with id " +
+          complexTypeId);
+    }
+    Map<String,Object> cColRow = cursor.getCurrentRow();
+    int tableId = (Integer)cColRow.get(COL_TABLE_ID);
+    if(tableId != column.getTable().getTableDefPageNumber()) {
+      throw new IOException(
+          "Found complex column for table " + tableId + " but expected table " +
+          column.getTable().getTableDefPageNumber());
+    }
+    int flatTableId = (Integer)cColRow.get(COL_FLAT_TABLE_ID);
+    int typeObjId = (Integer)cColRow.get(COL_COMPLEX_TYPE_OBJECT_ID);
+
+    Table typeObjTable = db.getTable(typeObjId);
+    Table flatTable = db.getTable(flatTableId);
+
+    if((typeObjTable == null) || (flatTable == null)) {
+      throw new IOException(
+          "Could not find supporting tables (" + typeObjId + ", " + flatTableId
+          + ") for complex column with id " + complexTypeId);
+    }
+    
+    // we inspect the structore of the "type table" to determine what kind of
+    // complex info we are dealing with
+    if(MultiValueColumnInfo.isMultiValueColumn(typeObjTable)) {
+      return new MultiValueColumnInfo(column, complexTypeId, typeObjTable,
+                                      flatTable);
+    } else if(AttachmentColumnInfo.isAttachmentColumn(typeObjTable)) {
+      return new AttachmentColumnInfo(column, complexTypeId, typeObjTable,
+                                      flatTable);
+    } else if(VersionHistoryColumnInfo.isVersionHistoryColumn(typeObjTable)) {
+      return new VersionHistoryColumnInfo(column, complexTypeId, typeObjTable,
+                                          flatTable);
+    }
+    
+    LOG.warn("Unsupported complex column type " + typeObjTable.getName());
+    return new UnsupportedColumnInfo(column, complexTypeId, typeObjTable,
+                                     flatTable);
+  }
+
+  public void postTableLoadInit() throws IOException {
+    // nothing to do in base class
+  }
+  
+  public Column getColumn() {
+    return _column;
+  }
+
+  public Database getDatabase() {
+    return getColumn().getDatabase();
+  }
+  
+  public JetFormat getFormat() {
+    return getDatabase().getFormat();
+  }
+
+  public PageChannel getPageChannel() {
+    return getDatabase().getPageChannel();
+  }
+
+  public Column getPrimaryKeyColumn() {
+    return _pkCol;
+  }
+
+  public Column getComplexValueForeignKeyColumn() {
+    return _complexValFkCol;
+  }
+
+  protected List<Column> getTypeColumns() {
+    return _typeCols;
+  }
+  
+  public int countValues(int complexValueFk)
+    throws IOException
+  {
+    return getRawValues(complexValueFk,
+                        Collections.singleton(_complexValFkCol.getName()))
+      .size();
+  }
+  
+  public List<Map<String,Object>> getRawValues(int complexValueFk)
+    throws IOException
+  {
+    return getRawValues(complexValueFk, null);
+  }
+  
+  public List<Map<String,Object>> getRawValues(int complexValueFk,
+                                               Collection<String> columnNames)
+    throws IOException
+  {
+    if(_complexValIdCursor == null) {
+      _complexValIdCursor = new CursorBuilder(_flatTable)
+        .setIndexByColumns(_complexValFkCol)
+        .toIndexCursor();
+    }
+
+    Iterator<Map<String,Object>> entryIter =
+      _complexValIdCursor.entryIterator(columnNames, complexValueFk);
+    if(!entryIter.hasNext()) {
+      return Collections.emptyList();
+    }
+
+    List<Map<String,Object>> values = new ArrayList<Map<String,Object>>();
+    while(entryIter.hasNext()) {
+      values.add(entryIter.next());
+    }
+    
+    return values;
+  }
+
+  public List<V> getValues(
+      ComplexValueForeignKey complexValueFk)
+    throws IOException
+  {
+    List<Map<String,Object>> rawValues = getRawValues(complexValueFk.get());
+    if(rawValues.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    return toValues(complexValueFk, rawValues);
+  }
+  
+  public int addRawValue(Map<String,Object> rawValue) throws IOException {
+    Object[] row = _flatTable.asRow(rawValue);
+    _flatTable.addRow(row);
+    return (Integer)_pkCol.getRowValue(row);
+  }
+
+  public int addValue(V value) throws IOException {
+    Object[] row = asRow(newRowArray(), value);
+    _flatTable.addRow(row);
+    int id = (Integer)_pkCol.getRowValue(row);
+    value.setId(id);
+    return id;
+  }
+
+  public void addValues(Collection<? extends V> values)
+    throws IOException
+  {
+    for(V value : values) {
+      addValue(value);
+    }
+  }
+
+  public int updateRawValue(Map<String,Object> rawValue) throws IOException {
+    Integer id = (Integer)_pkCol.getRowValue(rawValue);
+    updateRow(id, _flatTable.asUpdateRow(rawValue));
+    return id;
+  }
+  
+  public int updateValue(V value) throws IOException {
+    int id = value.getId();
+    updateRow(id, asRow(newRowArray(), value));
+    return id;
+  }
+
+  public void updateValues(Collection<? extends V> values)
+    throws IOException
+  {
+    for(V value : values) {
+      updateValue(value);
+    }
+  }
+
+  private void updateRow(Integer id, Object[] row) throws IOException {
+    if(_pkCursor == null) {
+      _pkCursor = new CursorBuilder(_flatTable)
+        .setIndexByColumns(_pkCol)
+        .toIndexCursor();
+    }
+
+    if(!_pkCursor.findRowByEntry(id)) {
+      throw new IllegalArgumentException("Row with id " + id +
+                                         " does not exist");
+    }
+    
+    _pkCursor.updateCurrentRow(row);
+  }
+  
+  protected Object[] asRow(Object[] row, V value) {
+    int id = value.getId();
+    _pkCol.setRowValue(row, ((id != INVALID_ID) ? id : Column.AUTO_NUMBER));
+    int cId = value.getComplexValueForeignKey().get();
+    _complexValFkCol.setRowValue(
+        row, ((cId != INVALID_ID) ? cId : Column.AUTO_NUMBER));
+    return row;
+  }
+
+  private Object[] newRowArray() {
+    return new Object[_flatTable.getColumnCount()];
+  }
+  
+  @Override
+  public String toString() {
+    StringBuilder rtn = new StringBuilder();
+    rtn.append("\n\t\tComplexType: " + getType());
+    rtn.append("\n\t\tComplexTypeId: " + _complexTypeId);
+    return rtn.toString();
+  }
+
+  protected static void diffFlatColumns(Table typeObjTable, Table flatTable,
+                                        List<Column> typeCols,
+                                        List<Column> otherCols)
+  {
+    // each "flat"" table has the columns from the "type" table, plus some
+    // others.  separate the "flat" columns into these 2 buckets
+    for(Column col : flatTable.getColumns()) {
+      boolean found = false;
+      try {
+        typeObjTable.getColumn(col.getName());
+        found = true;
+      } catch(IllegalArgumentException e) {
+        // FIXME better way to test this?
+      }
+      if(found) {
+        typeCols.add(col);
+      } else {
+        otherCols.add(col);
+      }  
+    } 
+  }
+  
+  public abstract ComplexDataType getType();
+
+  protected abstract List<V> toValues(
+      ComplexValueForeignKey complexValueFk,
+      List<Map<String,Object>> rawValues)
+    throws IOException;
+  
+  protected static class ComplexValueImpl implements ComplexValue
+  {
+    private int _id;
+    private ComplexValueForeignKey _complexValueFk;
+
+    protected ComplexValueImpl(int id, ComplexValueForeignKey complexValueFk) {
+      _id = id;
+      _complexValueFk = complexValueFk;
+    }
+
+    public int getId() {
+      return _id;
+    }
+
+    public void setId(int id) {
+      if(_id != INVALID_ID) {
+        throw new IllegalStateException("id may not be reset");
+      }
+      _id = id;
+    }
+    
+    public ComplexValueForeignKey getComplexValueForeignKey() {
+      return _complexValueFk;
+    }
+
+    public void setComplexValueForeignKey(ComplexValueForeignKey complexValueFk)
+    {
+      if(_complexValueFk != INVALID_COMPLEX_VALUE_ID) {
+        throw new IllegalStateException("complexValueFk may not be reset");
+      }
+      _complexValueFk = complexValueFk;
+    }
+
+    public Column getColumn() {
+      return _complexValueFk.getColumn();
+    }
+
+    public void update() throws IOException {
+      throw new UnsupportedOperationException(
+          "This column does not support value updates");
+    }
+    
+    @Override
+    public int hashCode() {
+      return ((_id * 37) ^ _complexValueFk.hashCode());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return ((this == o) ||
+              ((o != null) && (getClass() == o.getClass()) &&
+               (_id == ((ComplexValueImpl)o)._id) &&
+               _complexValueFk.equals(((ComplexValueImpl)o)._complexValueFk)));
+    }
+  }
+  
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexDataType.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexDataType.java
new file mode 100644 (file)
index 0000000..1c5e699
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public enum ComplexDataType 
+{
+  ATTACHMENT, MULTI_VALUE, VERSION_HISTORY, UNSUPPORTED;
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java
new file mode 100644 (file)
index 0000000..e65a09c
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.io.IOException;
+
+import com.healthmarketscience.jackcess.Column;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public interface ComplexValue 
+{
+  public int getId();
+
+  public void setId(int newId);
+  
+  public ComplexValueForeignKey getComplexValueForeignKey();
+
+  public void setComplexValueForeignKey(ComplexValueForeignKey complexValueFk);
+
+  public Column getColumn();
+
+  public void update() throws IOException;
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java
new file mode 100644 (file)
index 0000000..294958f
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.io.IOException;
+import java.io.ObjectStreamException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import com.healthmarketscience.jackcess.Column;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class ComplexValueForeignKey extends Number
+{
+  private static final long serialVersionUID = 20110805L;  
+  
+  private final Column _column;
+  private final int _value;
+  private List<? extends ComplexValue> _values;
+  
+  public ComplexValueForeignKey(Column column, int value) 
+  {
+    _column = column;
+    _value = value;
+  }
+
+  public int get() {
+    return _value;
+  }
+
+  public Column getColumn() {
+    return _column;
+  }
+  
+  @Override
+  public byte byteValue() {
+    return (byte)get();
+  }
+  
+  @Override
+  public short shortValue() {
+    return (short)get();
+  }
+  
+  @Override
+  public int intValue() {
+    return get();
+  }
+  
+  @Override
+  public long longValue() {
+    return get();
+  }
+  
+  @Override
+  public float floatValue() {
+    return get();
+  }
+  
+  @Override
+  public double doubleValue() {
+    return get();
+  }
+
+  public ComplexDataType getComplexType() {
+    return getComplexInfo().getType();
+  }
+  
+  protected ComplexColumnInfo<? extends ComplexValue> getComplexInfo() {
+    return _column.getComplexInfo();
+  }
+
+  protected VersionHistoryColumnInfo getVersionInfo() {
+    return (VersionHistoryColumnInfo)getComplexInfo();
+  }
+  
+  protected AttachmentColumnInfo getAttachmentInfo() {
+    return (AttachmentColumnInfo)getComplexInfo();
+  }
+
+  protected MultiValueColumnInfo getMultiValueInfo() {
+    return (MultiValueColumnInfo)getComplexInfo();
+  }
+    
+  public int countValues()
+    throws IOException
+  {
+    return getComplexInfo().countValues(get());
+  }
+  
+  public List<Map<String,Object>> getRawValues()
+    throws IOException
+  {
+    return getComplexInfo().getRawValues(get());
+  }  
+  
+  public List<? extends ComplexValue> getValues()
+    throws IOException
+  {
+    if(_values == null) {
+      _values = getComplexInfo().getValues(this);
+    }
+    return _values;
+  }  
+
+  @SuppressWarnings("unchecked")
+  public List<Version> getVersions()
+    throws IOException
+  {
+    if(getComplexType() != ComplexDataType.VERSION_HISTORY) {
+      throw new UnsupportedOperationException();
+    }
+    return (List<Version>)getValues();
+  }
+  
+  @SuppressWarnings("unchecked")
+  public List<Attachment> getAttachments()
+    throws IOException
+  {
+    if(getComplexType() != ComplexDataType.ATTACHMENT) {
+      throw new UnsupportedOperationException();
+    }
+    return (List<Attachment>)getValues();
+  }
+  
+  @SuppressWarnings("unchecked")
+  public List<SingleValue> getMultiValues()
+    throws IOException
+  {
+    if(getComplexType() != ComplexDataType.MULTI_VALUE) {
+      throw new UnsupportedOperationException();
+    }
+    return (List<SingleValue>)getValues();
+  }
+  
+  public void reset() {
+    // discard any cached values
+    _values = null;
+  }
+  
+  public Version addVersion(String value)
+    throws IOException
+  {
+    return addVersion(value, new Date());
+  }
+  
+  public Version addVersion(String value, Date modifiedDate)
+    throws IOException
+  {
+    reset();
+    Version v = VersionHistoryColumnInfo.newVersion(this, value, modifiedDate);
+    getVersionInfo().addValue(v);
+    return v;
+  }
+
+  public Attachment addAttachment(byte[] data)
+    throws IOException
+  {
+    return addAttachment(null, null, null, data, null, null);
+  }
+  
+  public Attachment addAttachment(
+      String url, String name, String type, byte[] data,
+      Date timeStamp, Integer flags)
+    throws IOException
+  {
+    reset();
+    Attachment a = AttachmentColumnInfo.newAttachment(
+        this, url, name, type, data, timeStamp, flags);
+    getAttachmentInfo().addValue(a);
+    return a;
+  }
+
+  public Attachment updateAttachment(Attachment attachment)
+    throws IOException
+  {
+    reset();
+    getAttachmentInfo().updateValue(attachment);
+    return attachment;
+  }
+  
+  public SingleValue addMultiValue(Object value)
+    throws IOException
+  {
+    reset();
+    SingleValue v = MultiValueColumnInfo.newSingleValue(this, value);
+    getMultiValueInfo().addValue(v);
+    return v;
+  }
+  
+  public SingleValue updateMultiValue(SingleValue value)
+    throws IOException
+  {
+    reset();
+    getMultiValueInfo().updateValue(value);
+    return value;
+  }
+  
+  private Object writeReplace() throws ObjectStreamException {
+    // if we are going to serialize this ComplexValueForeignKey, convert it
+    // back to a normal Integer (in case it is restored outside of the context
+    // of jackcess)
+    return Integer.valueOf(_value);
+  }
+  
+  @Override
+  public int hashCode() {
+    return _value;
+  }
+  
+  @Override
+  public boolean equals(Object o) {
+    return ((this == o) ||
+            ((o != null) && (getClass() == o.getClass()) &&
+             (_value == ((ComplexValueForeignKey)o)._value) &&
+             (_column == ((ComplexValueForeignKey)o)._column)));
+  }
+
+  @Override
+  public String toString()
+  {
+    return String.valueOf(_value);
+  }
+  
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java
new file mode 100644 (file)
index 0000000..1b117c6
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.DataType;
+import com.healthmarketscience.jackcess.Table;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class MultiValueColumnInfo extends ComplexColumnInfo<SingleValue>
+{
+  private static final Set<DataType> VALUE_TYPES = EnumSet.of(
+      DataType.BYTE, DataType.INT, DataType.LONG, DataType.FLOAT,
+      DataType.DOUBLE, DataType.GUID, DataType.NUMERIC, DataType.TEXT);
+
+  private final Column _valueCol;
+  
+  public MultiValueColumnInfo(Column column, int complexId,
+                              Table typeObjTable, Table flatTable) 
+    throws IOException
+  {
+    super(column, complexId, typeObjTable, flatTable);
+
+    _valueCol = getTypeColumns().get(0);
+  }
+
+  @Override
+  public ComplexDataType getType()
+  {
+    return ComplexDataType.MULTI_VALUE;
+  }
+
+  public Column getValueColumn() {
+    return _valueCol;
+  }
+  
+  @Override
+  protected List<SingleValue> toValues(ComplexValueForeignKey complexValueFk,
+                                       List<Map<String,Object>> rawValues)
+    throws IOException
+  {
+    List<SingleValue> values = new ArrayList<SingleValue>();
+    for(Map<String,Object> rawValue : rawValues) {
+      values.add(toSingleValue(complexValueFk, rawValue));
+    }
+
+    return values;
+  }
+
+  protected SingleValueImpl toSingleValue(
+      ComplexValueForeignKey complexValueFk,
+      Map<String,Object> rawValue)
+  {
+    int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue);
+    Object value = getValueColumn().getRowValue(rawValue);
+
+    return new SingleValueImpl(id, complexValueFk, value);
+  }
+
+  @Override
+  protected Object[] asRow(Object[] row, SingleValue value) {
+    super.asRow(row, value);
+    getValueColumn().setRowValue(row, value.get());
+    return row;
+  }
+  
+  public static SingleValue newSingleValue(Object value) {
+    return newSingleValue(INVALID_COMPLEX_VALUE_ID, value);
+  }
+
+  public static SingleValue newSingleValue(
+      ComplexValueForeignKey complexValueFk, Object value) {
+    return new SingleValueImpl(INVALID_ID, complexValueFk, value);
+  }
+
+  public static boolean isMultiValueColumn(Table typeObjTable) {
+    // if we found a single value of a "simple" type, then we are dealing with
+    // a multi-value column
+    List<Column> typeCols = typeObjTable.getColumns();
+    return ((typeCols.size() == 1) &&
+            VALUE_TYPES.contains(typeCols.get(0).getType()));
+  }
+
+  private static class SingleValueImpl extends ComplexValueImpl
+    implements SingleValue
+  {
+    private Object _value;
+
+    private SingleValueImpl(int id, ComplexValueForeignKey complexValueFk,
+                            Object value)
+    {
+      super(id, complexValueFk);
+      _value = value;
+    }
+    
+    public Object get() {
+      return _value;
+    }
+
+    public void set(Object value) {
+      _value = value;
+    }
+
+    @Override
+    public void update() throws IOException {
+      getComplexValueForeignKey().updateMultiValue(this);
+    }
+    
+    @Override
+    public String toString()
+    {
+      return "SingleValue(" + getComplexValueForeignKey() + "," + getId() +
+        ") " + get();
+    } 
+  }
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/SingleValue.java b/src/java/com/healthmarketscience/jackcess/complex/SingleValue.java
new file mode 100644 (file)
index 0000000..0859460
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (c) 2011 Boomi, Inc.
+
+package com.healthmarketscience.jackcess.complex;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public interface SingleValue extends ComplexValue 
+{
+  public Object get();
+
+  public void set(Object value);
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java
new file mode 100644 (file)
index 0000000..fd2a0fc
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.Table;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class UnsupportedColumnInfo extends ComplexColumnInfo<ComplexValue>
+{
+
+  public UnsupportedColumnInfo(Column column, int complexId, Table typeObjTable,
+                               Table flatTable)
+    throws IOException
+  {
+    super(column, complexId, typeObjTable, flatTable);
+  }
+
+  @Override
+  public ComplexDataType getType()
+  {
+    return ComplexDataType.UNSUPPORTED;
+  }
+
+  @Override
+  protected List<ComplexValue> toValues(ComplexValueForeignKey complexValueFk,
+                                        List<Map<String,Object>> rawValues)
+    throws IOException
+  {
+    // FIXME
+    return null;
+  }
+
+  public ComplexValue newValue() {
+    // FIXME
+    return null;
+  }
+  
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/Version.java b/src/java/com/healthmarketscience/jackcess/complex/Version.java
new file mode 100644 (file)
index 0000000..70477f8
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.util.Date;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public interface Version extends ComplexValue, Comparable<Version>
+{
+  public String getValue();
+
+  public Date getModifiedDate();
+}
diff --git a/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java
new file mode 100644 (file)
index 0000000..0cf2fe7
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.complex;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.Table;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class VersionHistoryColumnInfo extends ComplexColumnInfo<Version> 
+{
+  private final Column _valueCol;
+  private final Column _modifiedCol;
+  
+  public VersionHistoryColumnInfo(Column column, int complexId,
+                                  Table typeObjTable, Table flatTable) 
+    throws IOException
+  {
+    super(column, complexId, typeObjTable, flatTable);
+
+    Column valueCol = null;
+    Column modifiedCol = null;
+    for(Column col : getTypeColumns()) {
+      switch(col.getType()) {
+      case SHORT_DATE_TIME:
+        modifiedCol = col;
+        break;
+      case MEMO:
+        valueCol = col;
+        break;
+      default:
+        // ignore
+      }
+    }
+
+    _valueCol = valueCol;
+    _modifiedCol = modifiedCol;
+  }
+
+  @Override
+  public void postTableLoadInit() throws IOException {
+    super.postTableLoadInit();
+
+    // link up with the actual versioned column.  it should have the same name
+    // as the "value" column in the type table.
+    Column versionedCol = getColumn().getTable().getColumn(
+        getValueColumn().getName());
+    versionedCol.setVersionHistoryColumn(getColumn());
+  }
+    
+  public Column getValueColumn() {
+    return _valueCol;
+  }
+
+  public Column getModifiedDateColumn() {
+    return _modifiedCol;
+  }
+  
+  @Override
+  public ComplexDataType getType() {
+    return ComplexDataType.VERSION_HISTORY;
+  }
+
+  @Override
+  protected List<Version> toValues(ComplexValueForeignKey complexValueFk,
+                                   List<Map<String,Object>> rawValues)
+    throws IOException
+  {
+    List<Version> versions = new ArrayList<Version>();
+    for(Map<String,Object> rawValue : rawValues) {
+      versions.add(toVersion(complexValueFk, rawValue));
+    }
+
+    // order versions newest to oldest
+    Collections.sort(versions);
+    
+    return versions;
+  }
+
+  protected VersionImpl toVersion(ComplexValueForeignKey complexValueFk,
+                                  Map<String,Object> rawValue) {
+    int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue);
+    String value = (String)getValueColumn().getRowValue(rawValue);
+    Date modifiedDate = (Date)getModifiedDateColumn().getRowValue(rawValue);
+
+    return new VersionImpl(id, complexValueFk, value, modifiedDate);
+  }
+
+  @Override
+  protected Object[] asRow(Object[] row, Version version) {
+    super.asRow(row, version);
+    getValueColumn().setRowValue(row, version.getValue());
+    getModifiedDateColumn().setRowValue(row, version.getModifiedDate());
+    return row;
+  }
+  
+  public static Version newVersion(String value, Date modifiedDate) {
+    return newVersion(INVALID_COMPLEX_VALUE_ID, value, modifiedDate);
+  }
+  
+  public static Version newVersion(ComplexValueForeignKey complexValueFk,
+                                   String value, Date modifiedDate) {
+    return new VersionImpl(INVALID_ID, complexValueFk, value, modifiedDate);
+  }
+
+  public static boolean isVersionHistoryColumn(Table typeObjTable) {
+    // version history data has these columns <value>(MEMO),
+    // <modified>(SHORT_DATE_TIME)
+    List<Column> typeCols = typeObjTable.getColumns();
+    if(typeCols.size() < 2) {
+      return false;
+    }
+
+    int numMemo = 0;
+    int numDate = 0;
+    
+    for(Column col : typeCols) {
+      switch(col.getType()) {
+      case SHORT_DATE_TIME:
+        ++numDate;
+        break;
+      case MEMO:
+        ++numMemo;
+        break;
+      default:
+        // ignore
+      }
+    }
+
+    // be flexible, allow for extra columns...
+    return((numMemo >= 1) && (numDate >= 1));
+  }
+
+  private static class VersionImpl extends ComplexValueImpl implements Version
+  {
+    private final String _value;
+    private final Date _modifiedDate;
+
+    private VersionImpl(int id, ComplexValueForeignKey complexValueFk,
+                        String value, Date modifiedDate)
+    {
+      super(id, complexValueFk);
+      _value = value;
+      _modifiedDate = modifiedDate;
+    }
+    
+    public String getValue() {
+      return _value;
+    }
+
+    public Date getModifiedDate() {
+      return _modifiedDate;
+    }    
+    
+    public int compareTo(Version o) {
+      Date d1 = getModifiedDate();
+      Date d2 = o.getModifiedDate();
+
+      // sort by descending date (newest/greatest first)
+      int cmp = d2.compareTo(d1);
+      if(cmp != 0) {
+        return cmp;
+      }
+
+      // use id, then complexValueFk to break ties (although we really
+      // shouldn't be comparing across different columns)
+      int id1 = getId();
+      int id2 = o.getId();
+      if(id1 != id2) {
+        return ((id1 > id2) ? -1 : 1);
+      }
+      id1 = getComplexValueForeignKey().get();
+      id2 = o.getComplexValueForeignKey().get();
+      return ((id1 > id2) ? -1 :
+              ((id1 < id2) ? 1 : 0));
+    }
+
+    @Override
+    public String toString()
+    {
+      return "Version(" + getComplexValueForeignKey() + "," + getId() + ") " +
+        getModifiedDate() + ", " + getValue();
+    } 
+  }
+  
+}
diff --git a/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java b/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java
new file mode 100644 (file)
index 0000000..d853f6f
--- /dev/null
@@ -0,0 +1,309 @@
+// Copyright (c) 2011 Boomi, Inc.
+
+package com.healthmarketscience.jackcess;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import static com.healthmarketscience.jackcess.DatabaseTest.*;
+import com.healthmarketscience.jackcess.complex.Attachment;
+import com.healthmarketscience.jackcess.complex.ComplexDataType;
+import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
+import com.healthmarketscience.jackcess.complex.SingleValue;
+import com.healthmarketscience.jackcess.complex.Version;
+import junit.framework.TestCase;
+
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class ComplexColumnTest extends TestCase 
+{
+
+  public ComplexColumnTest(String name) {
+    super(name);
+  }
+
+  public void testVersions() throws Exception
+  {
+    Database db = DatabaseTest.openCopy(Database.FileFormat.V2007, new File("/data2/jackcess_test/complexDataTest.accdb"));
+    db.setTimeZone(TEST_TZ);
+
+    Table t1 = db.getTable("Table1");
+    Column col = t1.getColumn("append-memo-data");
+    assertTrue(col.isAppendOnly());
+    Column verCol = col.getVersionHistoryColumn();
+    assertNotNull(verCol);
+    assertEquals(ComplexDataType.VERSION_HISTORY,
+                 verCol.getComplexInfo().getType());
+
+    for(Map<String,Object> row : t1) {
+      String rowId = (String)row.get("id");
+      ComplexValueForeignKey complexValueFk =
+        (ComplexValueForeignKey)verCol.getRowValue(row);
+
+      String curValue = (String)col.getRowValue(row);
+      
+      if(rowId.equals("row1")) {
+        checkVersions(1, complexValueFk, curValue);
+      } else if(rowId.equals("row2")) {
+        checkVersions(2, complexValueFk, curValue,
+                      "row2-memo", new Date(1315876862334L));
+      } else if(rowId.equals("row3")) {
+        checkVersions(3, complexValueFk, curValue,
+                      "row3-memo-again", new Date(1315876965382L),
+                      "row3-memo-revised", new Date(1315876953077L),
+                      "row3-memo", new Date(1315876879126L));
+      } else if(rowId.equals("row4")) {
+        checkVersions(4, complexValueFk, curValue,
+                      "row4-memo", new Date(1315876945758L));
+      } else {
+        assertTrue(false);
+      }
+    }
+
+    Object[] row8 = {"row8", Column.AUTO_NUMBER, "some-data", "row8-memo",
+                     Column.AUTO_NUMBER, Column.AUTO_NUMBER};
+    t1.addRow(row8);
+
+    ComplexValueForeignKey row8ValFk = (ComplexValueForeignKey)
+      verCol.getRowValue(row8);
+    Date upTime = new Date();
+    row8ValFk.addVersion("row8-memo", upTime);
+    checkVersions(8, row8ValFk, "row8-memo",
+                  "row8-memo", upTime);    
+
+    Cursor cursor = Cursor.createCursor(t1);
+    assertTrue(cursor.findRow(t1.getColumn("id"), "row3"));
+    ComplexValueForeignKey row3ValFk = (ComplexValueForeignKey)
+      cursor.getCurrentRowValue(verCol);
+    cursor.setCurrentRowValue(col, "new-value");
+    Version v = row3ValFk.addVersion("new-value", upTime);
+    checkVersions(3, row3ValFk, "new-value",
+                  "new-value", upTime,
+                  "row3-memo-again", new Date(1315876965382L),
+                  "row3-memo-revised", new Date(1315876953077L),
+                  "row3-memo", new Date(1315876879126L));
+
+    try {
+      v.update();
+      fail("UnsupportedOperationException should have been thrown");
+    } catch(UnsupportedOperationException expected) {
+      // success
+    }
+    
+    db.close();
+  }
+
+  public void testAttachments() throws Exception
+  {
+    Database db = DatabaseTest.openCopy(Database.FileFormat.V2007, new File("/data2/jackcess_test/complexDataTest.accdb"));
+    db.setTimeZone(TEST_TZ);
+
+    Table t1 = db.getTable("Table1");
+    Column col = t1.getColumn("attach-data");
+    assertEquals(ComplexDataType.ATTACHMENT,
+                 col.getComplexInfo().getType());
+
+    for(Map<String,Object> row : t1) {
+      String rowId = (String)row.get("id");
+      ComplexValueForeignKey complexValueFk =
+        (ComplexValueForeignKey)col.getRowValue(row);
+
+      if(rowId.equals("row1")) {
+        checkAttachments(1, complexValueFk);
+      } else if(rowId.equals("row2")) {
+        checkAttachments(2, complexValueFk, "test_data.txt", "test_data2.txt");
+      } else if(rowId.equals("row3")) {
+        checkAttachments(3, complexValueFk);
+      } else if(rowId.equals("row4")) {
+        checkAttachments(4, complexValueFk, "test_data2.txt");
+      } else {
+        assertTrue(false);
+      }
+    }
+
+    Object[] row8 = {"row8", Column.AUTO_NUMBER, "some-data", "row8-memo",
+                     Column.AUTO_NUMBER, Column.AUTO_NUMBER};
+    t1.addRow(row8);
+
+    ComplexValueForeignKey row8ValFk = (ComplexValueForeignKey)
+      col.getRowValue(row8);
+    row8ValFk.addAttachment(null, "test_data.txt", "txt",
+                            getFileBytes("test_data.txt"), null, null);
+    checkAttachments(8, row8ValFk, "test_data.txt");
+
+    Cursor cursor = Cursor.createCursor(t1);
+    assertTrue(cursor.findRow(t1.getColumn("id"), "row4"));
+    ComplexValueForeignKey row4ValFk = (ComplexValueForeignKey)
+      cursor.getCurrentRowValue(col);
+    Attachment a = row4ValFk.addAttachment(null, "test_data.txt", "txt",
+                                           getFileBytes("test_data.txt"), null,
+                                           null);
+    checkAttachments(4, row4ValFk, "test_data2.txt", "test_data.txt");
+
+    a.setFileType("xml");
+    a.setFileName("some_data.xml");
+    byte[] newBytes = "this is not xml".getBytes("US-ASCII");
+    a.setFileData(newBytes);
+    a.update();
+
+    Attachment updated = row4ValFk.getAttachments().get(1);
+    assertNotSame(updated, a);
+    assertEquals("xml", updated.getFileType());
+    assertEquals("some_data.xml", updated.getFileName());
+    assertTrue(Arrays.equals(newBytes, updated.getFileData()));
+    
+    db.close();
+  }
+
+  public void testMultiValues() throws Exception
+  {
+    Database db = DatabaseTest.openCopy(Database.FileFormat.V2007, new File("/data2/jackcess_test/complexDataTest.accdb"));
+    db.setTimeZone(TEST_TZ);
+
+    Table t1 = db.getTable("Table1");
+    Column col = t1.getColumn("multi-value-data");
+    assertEquals(ComplexDataType.MULTI_VALUE,
+                 col.getComplexInfo().getType());
+
+    for(Map<String,Object> row : t1) {
+      String rowId = (String)row.get("id");
+      ComplexValueForeignKey complexValueFk =
+        (ComplexValueForeignKey)col.getRowValue(row);
+
+      if(rowId.equals("row1")) {
+        checkMultiValues(1, complexValueFk);
+      } else if(rowId.equals("row2")) {
+        checkMultiValues(2, complexValueFk, "value1", "value4");
+      } else if(rowId.equals("row3")) {
+        checkMultiValues(3, complexValueFk,
+                         "value1", "value2", "value3", "value4");
+      } else if(rowId.equals("row4")) {
+        checkMultiValues(4, complexValueFk);
+      } else {
+        assertTrue(false);
+      }
+    }     
+
+    Object[] row8 = {"row8", Column.AUTO_NUMBER, "some-data", "row8-memo",
+                     Column.AUTO_NUMBER, Column.AUTO_NUMBER};
+    t1.addRow(row8);
+
+    ComplexValueForeignKey row8ValFk = (ComplexValueForeignKey)
+      col.getRowValue(row8);
+    row8ValFk.addMultiValue("value1");
+    row8ValFk.addMultiValue("value2");
+    checkMultiValues(8, row8ValFk, "value1", "value2");
+
+    Cursor cursor = Cursor.createCursor(t1);
+    assertTrue(cursor.findRow(t1.getColumn("id"), "row2"));
+    ComplexValueForeignKey row2ValFk = (ComplexValueForeignKey)
+      cursor.getCurrentRowValue(col);
+    SingleValue v = row2ValFk.addMultiValue("value2");
+    row2ValFk.addMultiValue("value3");
+    checkMultiValues(2, row2ValFk, "value1", "value4", "value2", "value3");
+
+    v.set("value5");
+    v.update();
+    checkMultiValues(2, row2ValFk, "value1", "value4", "value5", "value3");
+    
+    db.close();
+  }
+  
+  private static void checkVersions(
+      int cValId, ComplexValueForeignKey complexValueFk,
+      String curValue, Object... versionInfos)
+    throws Exception
+  {
+    assertEquals(cValId, complexValueFk.get());
+
+    List<Version> versions = complexValueFk.getVersions();
+    if(versionInfos.length == 0) {
+      assertTrue(versions.isEmpty());
+      assertNull(curValue);
+    } else {
+      assertEquals(versionInfos.length / 2, versions.size());
+      assertEquals(curValue, versions.get(0).getValue());
+      for(int i = 0; i < versionInfos.length; i+=2) {
+        String value = (String)versionInfos[i];
+        Date modDate = (Date)versionInfos[i+1];
+        Version v = versions.get(i/2);
+        assertEquals(value, v.getValue());
+        assertSameDate(modDate, v.getModifiedDate());
+      }
+    }
+  }
+
+  private static void checkAttachments(
+      int cValId, ComplexValueForeignKey complexValueFk,
+      String... fileNames)
+    throws Exception
+  {
+    assertEquals(cValId, complexValueFk.get());
+    
+    List<Attachment> attachments = complexValueFk.getAttachments();
+    if(fileNames.length == 0) {
+      assertTrue(attachments.isEmpty());
+    } else {
+      assertEquals(fileNames.length, attachments.size());
+      for(int i = 0; i < fileNames.length; ++i) {
+        String fname = fileNames[i];
+        byte[] dataBytes = getFileBytes(fname);
+        Attachment a = attachments.get(i);
+        assertEquals(fname, a.getFileName());
+        assertEquals("txt", a.getFileType());
+        assertTrue(Arrays.equals(dataBytes, a.getFileData()));
+      }
+    }
+  }
+  
+  private static void checkMultiValues(
+      int cValId, ComplexValueForeignKey complexValueFk,
+      Object... expectedValues)
+    throws Exception
+  {
+    assertEquals(cValId, complexValueFk.get());
+
+    List<SingleValue> values = complexValueFk.getMultiValues();
+    if(expectedValues.length == 0) {
+      assertTrue(values.isEmpty());
+    } else {
+      assertEquals(expectedValues.length, values.size());
+      for(int i = 0; i < expectedValues.length; ++i) {
+        Object value = expectedValues[i];
+        SingleValue v = values.get(i);
+        assertEquals(value, v.get());
+      }
+    }    
+  }
+
+  private static byte[] getFileBytes(String fname) throws Exception
+  {
+    if("test_data.txt".equals(fname)) {
+      return TEST_BYTES;
+    }
+    if("test_data2.txt".equals(fname)) {
+      return TEST2_BYTES;
+    }
+    throw new RuntimeException("unexpected bytes");
+  }
+  
+  private static byte b(int i) { return (byte)i; }
+  
+  private static final byte[] TEST_BYTES = new byte[] {
+    b(0x01),b(0x00),b(0x00),b(0x00),b(0x3A),b(0x00),b(0x00),b(0x00),b(0x78),b(0x5E),b(0x13),b(0x61),b(0x60),b(0x60),b(0x60),b(0x04),b(0x62),b(0x16),b(0x20),b(0x2E),b(0x61),b(0xA8),b(0x00),b(0x62),
+    b(0x20),b(0x9D),b(0x91),b(0x59),b(0xAC),b(0x00),b(0x44),b(0xC5),b(0xF9),b(0xB9),b(0xA9),b(0x0A),b(0x25),b(0xA9),b(0xC5),b(0x25),b(0x0A),b(0x29),b(0x89),b(0x25),b(0x89),b(0x0A),b(0x69),b(0xF9),
+    b(0x45),b(0x0A),b(0x89),b(0x25),b(0x25),b(0x89),b(0xC9),b(0x19),b(0xB9),b(0xA9),b(0x79),b(0x25),b(0x7A),b(0x00),b(0x52),b(0xA9),b(0x0F),b(0x7A)
+  };
+  
+  private static final byte[] TEST2_BYTES = new byte[] {
+    b(0x01),b(0x00),b(0x00),b(0x00),b(0x3F),b(0x00),b(0x00),b(0x00),b(0x78),b(0x5E),b(0x13),b(0x61),b(0x60),b(0x60),b(0x60),b(0x04),b(0x62),b(0x16),b(0x20),b(0x2E),b(0x61),b(0xA8),b(0x00),b(0x62),
+    b(0x20),b(0x9D),b(0x91),b(0x59),b(0xAC),b(0x00),b(0x44),b(0xC5),b(0xF9),b(0xB9),b(0xA9),b(0x0A),b(0xB9),b(0xF9),b(0x45),b(0xA9),b(0x0A),b(0x25),b(0xA9),b(0xC5),b(0x25),b(0x0A),b(0x29),b(0x89),
+    b(0x25),b(0x89),b(0x0A),b(0x69),b(0xF9),b(0x45),b(0x0A),b(0x89),b(0x25),b(0x25),b(0x89),b(0xC9),b(0x19),b(0xB9),b(0xA9),b(0x79),b(0x25),b(0x7A),b(0x00),b(0xA5),b(0x0B),b(0x11),b(0x4D)
+  };
+}
index 3b0837090aba60c981422f3026c56559fbfc0d12..1f118ca01551c3ded2df03faf9f7b792f3fbf73c 100644 (file)
@@ -51,11 +51,13 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TimeZone;
 import java.util.TreeSet;
 import java.util.UUID;
 
 import static com.healthmarketscience.jackcess.Database.*;
 import static com.healthmarketscience.jackcess.JetFormatTest.*;
+import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
 import junit.framework.TestCase;
 
 /**
@@ -63,6 +65,9 @@ import junit.framework.TestCase;
  */
 public class DatabaseTest extends TestCase {
 
+  public static final TimeZone TEST_TZ =
+    TimeZone.getTimeZone("America/New_York");
+  
   static boolean _autoSync = Database.DEFAULT_AUTO_SYNC;
 
 
@@ -972,22 +977,7 @@ public class DatabaseTest extends TestCase {
       for(int i = 0; i < dates.size(); ++i) {
         Date expected = dates.get(i);
         Date found = foundDates.get(i);
-        if(expected == null) {
-          assertNull(found);
-        } else {
-          // there are some rounding issues due to dates being stored as
-          // doubles, but it results in a 1 millisecond difference, so i'm not
-          // going to worry about it
-          long expTime = expected.getTime();
-          long foundTime = found.getTime();
-          try {
-            assertTrue((expTime == foundTime) ||
-                       (Math.abs(expTime - foundTime) <= 1));
-          } catch(Error e) {
-            System.err.println("Expected " + expTime + ", found " + foundTime);
-            throw e;
-          }
-        }
+        assertSameDate(expected, found);
       }
     }
   }
@@ -1253,18 +1243,31 @@ public class DatabaseTest extends TestCase {
   }    
   
   static void dumpDatabase(Database mdb) throws Exception {
-    dumpDatabase(mdb, new PrintWriter(System.out, true));
+    dumpDatabase(mdb, false);
+  }
+
+  static void dumpDatabase(Database mdb, boolean systemTables)
+    throws Exception
+  {
+    dumpDatabase(mdb, systemTables, new PrintWriter(System.out, true));
   }
 
   static void dumpTable(Table table) throws Exception {
     dumpTable(table, new PrintWriter(System.out, true));
   }
 
-  static void dumpDatabase(Database mdb, PrintWriter writer) throws Exception {
+  static void dumpDatabase(Database mdb, boolean systemTables,
+                           PrintWriter writer) throws Exception
+  {
     writer.println("DATABASE:");
     for(Table table : mdb) {
       dumpTable(table, writer);
     }
+    if(systemTables) {
+      for(String sysTableName : mdb.getSystemTableNames()) {
+        dumpTable(mdb.getSystemTable(sysTableName), writer);
+      }
+    }
   }
 
   static void dumpTable(Table table, PrintWriter writer) throws Exception {
@@ -1280,18 +1283,28 @@ public class DatabaseTest extends TestCase {
     }
     writer.println("COLUMNS: " + colNames);
     for(Map<String, Object> row : Cursor.createCursor(table)) {
+      writer.println(massageRow(row));
+    }
+  }
 
-      // make byte[] printable
+  private static Map<String,Object> massageRow(Map<String, Object> row)
+    throws IOException
+  {
       for(Map.Entry<String, Object> entry : row.entrySet()) {
         Object v = entry.getValue();
         if(v instanceof byte[]) {
+          // make byte[] printable
           byte[] bv = (byte[])v;
           entry.setValue(ByteUtil.toHexString(ByteBuffer.wrap(bv), bv.length));
+        } else if(v instanceof ComplexValueForeignKey) {
+          // deref complex values
+          String str = "ComplexValue(" + v + ")" +
+            ((ComplexValueForeignKey)v).getValues();
+          entry.setValue(str);
         }
       }
-      
-      writer.println(row);
-    }
+
+      return row;
   }
 
   static void dumpIndex(Index index) throws Exception {
@@ -1308,6 +1321,25 @@ public class DatabaseTest extends TestCase {
     }
   }
 
+  static void assertSameDate(Date expected, Date found)
+  {
+    if(expected == found) {
+      return;
+    }
+    if((expected == null) || (found == null)) {
+      throw new AssertionError("Expected " + expected + ", found " + found);
+    }
+    long expTime = expected.getTime();
+    long foundTime = found.getTime();
+    // there are some rounding issues due to dates being stored as doubles,
+    // but it results in a 1 millisecond difference, so i'm not going to worry
+    // about it
+    if((expTime != foundTime) && (Math.abs(expTime - foundTime) > 1)) {
+      throw new AssertionError("Expected " + expTime + " (" + expected +
+                               "), found " + foundTime + " (" + found + ")");
+    }
+  }
+  
   static void copyFile(File srcFile, File dstFile)
     throws IOException
   {
index ef0fa233155a0bcdaf1eb3ec6fa62fc1a7d26712..86b8fa6f1832feb7b589bff0577ed256eb46de0e 100644 (file)
@@ -31,7 +31,6 @@ import java.io.BufferedWriter;
 import java.io.StringWriter;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
-import java.util.TimeZone;
 
 import junit.framework.TestCase;
 import org.apache.commons.lang.SystemUtils;
@@ -55,14 +54,12 @@ public class ExportTest extends TestCase
 
   public void testExportToFile() throws Exception
   {
-    TimeZone testTZ = TimeZone.getTimeZone("America/New_York");
-
     DateFormat df = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
-    df.setTimeZone(testTZ);
+    df.setTimeZone(TEST_TZ);
 
     for (final FileFormat fileFormat : JetFormatTest.SUPPORTED_FILEFORMATS) {
       Database db = create(fileFormat);
-      db.setTimeZone(testTZ);
+      db.setTimeZone(TEST_TZ);
 
       Table t = new TableBuilder("test")
         .addColumn(new ColumnBuilder("col1", DataType.TEXT))
index ae2dd89c0e465c70565bcf4d90720895f51ddeea..7672e96972021bf6e9c01b91caf056f5befc30d1 100644 (file)
@@ -121,7 +121,7 @@ public class TableTest extends TestCase {
   {
     return _testTable.createRow(
         row, _testTable.getFormat().MAX_ROW_SIZE,
-        _testTable.getPageChannel().createPageBuffer(), false, 0);
+        _testTable.getPageChannel().createPageBuffer(), 0);
   }
 
   private ByteBuffer[] encodeColumns(Object... row)