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
<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.
}
/**
- * 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)
{
}
/**
- * 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)
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;
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;
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);
} 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();
+ }
}
/**
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();
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);
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");
}
}
}
+
+ 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
(_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 {
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()) {
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();
}
* <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.
}
@Override
- public Object getNext() {
+ public Object getNext(Object prevRowValue) {
// the table stores the last long autonumber used
return getTable().getNextLongAutoNumber();
}
}
@Override
- public Object getNext() {
+ public Object getNext(Object prevRowValue) {
// format guids consistently w/ Column.readGUIDValue()
_lastAutoNumber = "{" + UUID.randomUUID() + "}";
return _lastAutoNumber;
}
}
+ 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;
}
@Override
- public Object getNext() {
+ public Object getNext(Object prevRowValue) {
throw new UnsupportedOperationException();
}
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;
}
}
setScale(template.getScale());
setPrecision(template.getPrecision());
}
+ setCompressedUnicode(template.isCompressedUnicode());
return this;
}
{
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);
}
* 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.
}
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() {
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: " +
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");
}
}
}
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);
// 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
case INT:
case LONG:
case MONEY:
+ case COMPLEX_TYPE:
return new IntegerColumnDescriptor(col, flags);
case FLOAT:
case DOUBLE:
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;
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;
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();
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();
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();
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();
protected abstract Map<String,Database.FileFormat> getPossibleFileFormats();
+ protected abstract boolean isSupportedDataType(DataType type);
+
@Override
public String toString() {
return _name;
@Override
protected int defineOffsetNextAutoNumber() { return 20; }
@Override
+ protected int defineOffsetNextComplexAutoNumber() { return -1; }
+ @Override
protected int defineOffsetTableType() { return 20; }
@Override
protected int defineOffsetMaxCols() { return 21; }
@Override
protected int defineOffsetColumnCodePage() { return 11; }
@Override
+ protected int defineOffsetColumnComplexId() { return -1; }
+ @Override
protected int defineOffsetColumnFlags() { return 13; }
@Override
protected int defineOffsetColumnCompressedUnicode() { return 16; }
return PossibleFileFormats.POSSIBLE_VERSION_3;
}
+ @Override
+ protected boolean isSupportedDataType(DataType type) {
+ return (type != DataType.COMPLEX_TYPE);
+ }
}
private static class Jet4Format extends 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; }
@Override
protected int defineOffsetColumnCodePage() { return -1; }
@Override
+ protected int defineOffsetColumnComplexId() { return -1; }
+ @Override
protected int defineOffsetColumnFlags() { return 15; }
@Override
protected int defineOffsetColumnCompressedUnicode() { return 16; }
return PossibleFileFormats.POSSIBLE_VERSION_4;
}
+ @Override
+ protected boolean isSupportedDataType(DataType type) {
+ return (type != DataType.COMPLEX_TYPE);
+ }
}
private static final class MSISAMFormat extends Jet4Format {
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 {
@Override
public boolean matches(Map<String, Object> row)
{
- return ObjectUtils.equals(valuePattern, row.get(columnPattern.getName()));
+ return ObjectUtils.equals(valuePattern, columnPattern.getRowValue(row));
}
};
}
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) */
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>();
_tableErrorHandler = newErrorHandler;
}
- protected int getTableDefPageNumber() {
+ public int getTableDefPageNumber() {
return _tableDefPageNumber;
}
}
_maxColumnCount = (short)_columns.size();
_maxVarColumnCount = (short)_varColumns.size();
+ _autoNumColumns = getAutoNumberColumns(columns);
}
/**
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;
}
_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);
column.setName(readName(tableBuffer));
}
Collections.sort(_columns);
+ _autoNumColumns = getAutoNumberColumns(_columns);
// setup the data index for the columns
int colIdx = 0;
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();
+ }
}
/**
}
for(Column col : _columns) {
if(rowMap.containsKey(col.getName())) {
- row[col.getColumnIndex()] = rowMap.get(col.getName());
+ col.setRowValue(row, col.getRowValue(rowMap));
}
}
return row;
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() +
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() +
_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) {
* @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);
continue;
}
- Object rowValue = rowArray[col.getColumnIndex()];
+ Object rowValue = col.getRowValue(rowArray);
if (col.getType() == DataType.BOOLEAN) {
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) {
// 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
// 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;
}
}
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);
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)
{
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();
* @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);
--- /dev/null
+/*
+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);
+}
--- /dev/null
+/*
+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());
+ }
+ }
+
+}
--- /dev/null
+/*
+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)));
+ }
+ }
+
+}
--- /dev/null
+/*
+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;
+}
--- /dev/null
+/*
+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;
+}
--- /dev/null
+/*
+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);
+ }
+
+}
--- /dev/null
+/*
+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();
+ }
+ }
+}
--- /dev/null
+// 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);
+}
--- /dev/null
+/*
+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;
+ }
+
+}
--- /dev/null
+/*
+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();
+}
--- /dev/null
+/*
+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();
+ }
+ }
+
+}
--- /dev/null
+// 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)
+ };
+}
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;
/**
*/
public class DatabaseTest extends TestCase {
+ public static final TimeZone TEST_TZ =
+ TimeZone.getTimeZone("America/New_York");
+
static boolean _autoSync = Database.DEFAULT_AUTO_SYNC;
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);
}
}
}
}
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 {
}
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 {
}
}
+ 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
{
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;
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))
{
return _testTable.createRow(
row, _testTable.getFormat().MAX_ROW_SIZE,
- _testTable.getPageChannel().createPageBuffer(), false, 0);
+ _testTable.getPageChannel().createPageBuffer(), 0);
}
private ByteBuffer[] encodeColumns(Object... row)