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