From b014c0d170abc6bdca38078aaf1897aa66125152 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Wed, 26 Oct 2011 02:45:20 +0000 Subject: [PATCH] Add support for reading/writing complex column data (version history, attachments, multi-value columns) git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@580 f203690c-595d-4dc9-a70b-905162fa7fd2 --- TODO.txt | 2 + src/changes/changes.xml | 6 + .../jackcess/ByteUtil.java | 4 +- .../healthmarketscience/jackcess/Column.java | 135 +++++- .../jackcess/ColumnBuilder.java | 1 + .../healthmarketscience/jackcess/Cursor.java | 2 +- .../jackcess/DataType.java | 11 +- .../jackcess/Database.java | 23 +- .../jackcess/ExportUtil.java | 2 +- .../jackcess/IndexData.java | 1 + .../jackcess/JetFormat.java | 35 ++ .../jackcess/RowFilter.java | 2 +- .../healthmarketscience/jackcess/Table.java | 125 ++++-- .../jackcess/complex/Attachment.java | 53 +++ .../complex/AttachmentColumnInfo.java | 326 ++++++++++++++ .../jackcess/complex/ComplexColumnInfo.java | 400 ++++++++++++++++++ .../jackcess/complex/ComplexDataType.java | 29 ++ .../jackcess/complex/ComplexValue.java | 43 ++ .../complex/ComplexValueForeignKey.java | 246 +++++++++++ .../complex/MultiValueColumnInfo.java | 143 +++++++ .../jackcess/complex/SingleValue.java | 14 + .../complex/UnsupportedColumnInfo.java | 63 +++ .../jackcess/complex/Version.java | 33 ++ .../complex/VersionHistoryColumnInfo.java | 212 ++++++++++ .../jackcess/ComplexColumnTest.java | 309 ++++++++++++++ .../jackcess/DatabaseTest.java | 76 +++- .../jackcess/ExportTest.java | 7 +- .../jackcess/TableTest.java | 2 +- 28 files changed, 2232 insertions(+), 73 deletions(-) create mode 100644 src/java/com/healthmarketscience/jackcess/complex/Attachment.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/ComplexDataType.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/SingleValue.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/Version.java create mode 100644 src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java create mode 100644 test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java diff --git a/TODO.txt b/TODO.txt index 79abf42..3793dbf 100644 --- 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 diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b045cf8..25bc6bd 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -4,6 +4,12 @@ Tim McCune + + + Add support for reading/writing complex column data (version history, + attachments, multi-value columns). + + Try multiple classloaders when loading resources as streams. diff --git a/src/java/com/healthmarketscience/jackcess/ByteUtil.java b/src/java/com/healthmarketscience/jackcess/ByteUtil.java index c15fc7a..b39898a 100644 --- a/src/java/com/healthmarketscience/jackcess/ByteUtil.java +++ b/src/java/com/healthmarketscience/jackcess/ByteUtil.java @@ -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) diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index 78dd81b..847fd2b 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -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 { 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 _complexInfo; /** properties for this column, if any */ private PropertyMap _props; @@ -250,7 +256,7 @@ public class Column implements Comparable { 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 { } 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 { 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 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 { 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 { 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 { } } } + + public Object setRowValue(Object[] rowArray, Object value) { + rowArray[_columnIndex] = value; + return value; + } + + public Object setRowValue(Map rowMap, Object value) { + rowMap.put(_name, value); + return value; + } + + public Object getRowValue(Object[] rowArray) { + return rowArray[_columnIndex]; + } + + public Object getRowValue(Map 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 { (_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 { 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 { 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 { * Warning, calling this externally will result in this value being * "lost" for the table. */ - 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 { } @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 { } @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 { } } + 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 { } @Override - public Object getNext() { + public Object getNext(Object prevRowValue) { throw new UnsupportedOperationException(); } @@ -2253,5 +2375,8 @@ public class Column implements Comparable { 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; } } diff --git a/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java b/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java index 06e7356..befff67 100644 --- a/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java @@ -151,6 +151,7 @@ public class ColumnBuilder { setScale(template.getScale()); setPrecision(template.getPrecision()); } + setCompressedUnicode(template.isCompressedUnicode()); return this; } diff --git a/src/java/com/healthmarketscience/jackcess/Cursor.java b/src/java/com/healthmarketscience/jackcess/Cursor.java index 045fcea..623bebd 100644 --- a/src/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/java/com/healthmarketscience/jackcess/Cursor.java @@ -927,7 +927,7 @@ public abstract class Cursor implements Iterable> { 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); } diff --git a/src/java/com/healthmarketscience/jackcess/DataType.java b/src/java/com/healthmarketscience/jackcess/DataType.java index bc5e13d..8d20c1d 100644 --- a/src/java/com/healthmarketscience/jackcess/DataType.java +++ b/src/java/com/healthmarketscience/jackcess/DataType.java @@ -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() { diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index 549be7f..4eb0b9b 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -1253,6 +1253,13 @@ public class Database Set colNames = new HashSet(); // 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 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 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 aceRows = new ArrayList(_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); diff --git a/src/java/com/healthmarketscience/jackcess/ExportUtil.java b/src/java/com/healthmarketscience/jackcess/ExportUtil.java index 6c903fb..7876fb0 100644 --- a/src/java/com/healthmarketscience/jackcess/ExportUtil.java +++ b/src/java/com/healthmarketscience/jackcess/ExportUtil.java @@ -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 diff --git a/src/java/com/healthmarketscience/jackcess/IndexData.java b/src/java/com/healthmarketscience/jackcess/IndexData.java index 11b7b0a..b979315 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexData.java +++ b/src/java/com/healthmarketscience/jackcess/IndexData.java @@ -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: diff --git a/src/java/com/healthmarketscience/jackcess/JetFormat.java b/src/java/com/healthmarketscience/jackcess/JetFormat.java index cfc5d97..bd33fdc 100644 --- a/src/java/com/healthmarketscience/jackcess/JetFormat.java +++ b/src/java/com/healthmarketscience/jackcess/JetFormat.java @@ -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 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 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 { diff --git a/src/java/com/healthmarketscience/jackcess/RowFilter.java b/src/java/com/healthmarketscience/jackcess/RowFilter.java index 4f519e8..3a537af 100644 --- a/src/java/com/healthmarketscience/jackcess/RowFilter.java +++ b/src/java/com/healthmarketscience/jackcess/RowFilter.java @@ -107,7 +107,7 @@ public abstract class RowFilter @Override public boolean matches(Map row) { - return ObjectUtils.equals(valuePattern, row.get(columnPattern.getName())); + return ObjectUtils.equals(valuePattern, columnPattern.getRowValue(row)); } }; } diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index 30a48c7..0cf32ea 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -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 _columns = new ArrayList(); /** List of variable length columns in this table, ordered by offset */ private List _varColumns = new ArrayList(); + /** List of autonumber columns in this table, ordered by column number */ + private List _autoNumColumns; /** List of indexes on this table (multiple logical indexes may be backed by the same index data) */ private List _indexes = new ArrayList(); @@ -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 getAutoNumberColumns(Collection columns) { - List autoCols = new ArrayList(); + List autoCols = new ArrayList(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 index 0000000..af33544 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/Attachment.java @@ -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 index 0000000..f258ca0 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java @@ -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 +{ + 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 toValues(ComplexValueForeignKey complexValueFk, + List> rawValues) + throws IOException + { + List attachments = new ArrayList(); + for(Map rawValue : rawValues) { + attachments.add(toAttachment(complexValueFk, rawValue)); + } + return attachments; + } + + protected AttachmentImpl toAttachment(ComplexValueForeignKey complexValueFk, + Map 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 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 index 0000000..5232637 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java @@ -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 +{ + 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 _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(); + List otherCols = new ArrayList(); + 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 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 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 getTypeColumns() { + return _typeCols; + } + + public int countValues(int complexValueFk) + throws IOException + { + return getRawValues(complexValueFk, + Collections.singleton(_complexValFkCol.getName())) + .size(); + } + + public List> getRawValues(int complexValueFk) + throws IOException + { + return getRawValues(complexValueFk, null); + } + + public List> getRawValues(int complexValueFk, + Collection columnNames) + throws IOException + { + if(_complexValIdCursor == null) { + _complexValIdCursor = new CursorBuilder(_flatTable) + .setIndexByColumns(_complexValFkCol) + .toIndexCursor(); + } + + Iterator> entryIter = + _complexValIdCursor.entryIterator(columnNames, complexValueFk); + if(!entryIter.hasNext()) { + return Collections.emptyList(); + } + + List> values = new ArrayList>(); + while(entryIter.hasNext()) { + values.add(entryIter.next()); + } + + return values; + } + + public List getValues( + ComplexValueForeignKey complexValueFk) + throws IOException + { + List> rawValues = getRawValues(complexValueFk.get()); + if(rawValues.isEmpty()) { + return Collections.emptyList(); + } + + return toValues(complexValueFk, rawValues); + } + + public int addRawValue(Map 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 values) + throws IOException + { + for(V value : values) { + addValue(value); + } + } + + public int updateRawValue(Map 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 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 typeCols, + List 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 toValues( + ComplexValueForeignKey complexValueFk, + List> 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 index 0000000..1c5e699 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexDataType.java @@ -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 index 0000000..e65a09c --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java @@ -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 index 0000000..294958f --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java @@ -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 _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 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> getRawValues() + throws IOException + { + return getComplexInfo().getRawValues(get()); + } + + public List getValues() + throws IOException + { + if(_values == null) { + _values = getComplexInfo().getValues(this); + } + return _values; + } + + @SuppressWarnings("unchecked") + public List getVersions() + throws IOException + { + if(getComplexType() != ComplexDataType.VERSION_HISTORY) { + throw new UnsupportedOperationException(); + } + return (List)getValues(); + } + + @SuppressWarnings("unchecked") + public List getAttachments() + throws IOException + { + if(getComplexType() != ComplexDataType.ATTACHMENT) { + throw new UnsupportedOperationException(); + } + return (List)getValues(); + } + + @SuppressWarnings("unchecked") + public List getMultiValues() + throws IOException + { + if(getComplexType() != ComplexDataType.MULTI_VALUE) { + throw new UnsupportedOperationException(); + } + return (List)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 index 0000000..1b117c6 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java @@ -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 +{ + private static final Set 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 toValues(ComplexValueForeignKey complexValueFk, + List> rawValues) + throws IOException + { + List values = new ArrayList(); + for(Map rawValue : rawValues) { + values.add(toSingleValue(complexValueFk, rawValue)); + } + + return values; + } + + protected SingleValueImpl toSingleValue( + ComplexValueForeignKey complexValueFk, + Map 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 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 index 0000000..0859460 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/SingleValue.java @@ -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 index 0000000..fd2a0fc --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java @@ -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 +{ + + 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 toValues(ComplexValueForeignKey complexValueFk, + List> 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 index 0000000..70477f8 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/Version.java @@ -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 +{ + 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 index 0000000..0cf2fe7 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java @@ -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 +{ + 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 toValues(ComplexValueForeignKey complexValueFk, + List> rawValues) + throws IOException + { + List versions = new ArrayList(); + for(Map rawValue : rawValues) { + versions.add(toVersion(complexValueFk, rawValue)); + } + + // order versions newest to oldest + Collections.sort(versions); + + return versions; + } + + protected VersionImpl toVersion(ComplexValueForeignKey complexValueFk, + Map 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 (MEMO), + // (SHORT_DATE_TIME) + List 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 index 0000000..d853f6f --- /dev/null +++ b/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java @@ -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 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 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 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 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 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 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) + }; +} diff --git a/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java b/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java index 3b08370..1f118ca 100644 --- a/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -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 row : Cursor.createCursor(table)) { + writer.println(massageRow(row)); + } + } - // make byte[] printable + private static Map massageRow(Map row) + throws IOException + { for(Map.Entry 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 { diff --git a/test/src/java/com/healthmarketscience/jackcess/ExportTest.java b/test/src/java/com/healthmarketscience/jackcess/ExportTest.java index ef0fa23..86b8fa6 100644 --- a/test/src/java/com/healthmarketscience/jackcess/ExportTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/ExportTest.java @@ -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)) diff --git a/test/src/java/com/healthmarketscience/jackcess/TableTest.java b/test/src/java/com/healthmarketscience/jackcess/TableTest.java index ae2dd89..7672e96 100644 --- a/test/src/java/com/healthmarketscience/jackcess/TableTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/TableTest.java @@ -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) -- 2.39.5