소스 검색

Add support for reading/writing complex column data (version history, attachments, multi-value columns)

git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@580 f203690c-595d-4dc9-a70b-905162fa7fd2
tags/jackcess-1.2.6
James Ahlborn 12 년 전
부모
커밋
b014c0d170
28개의 변경된 파일2232개의 추가작업 그리고 73개의 파일을 삭제
  1. 2
    0
      TODO.txt
  2. 6
    0
      src/changes/changes.xml
  3. 2
    2
      src/java/com/healthmarketscience/jackcess/ByteUtil.java
  4. 130
    5
      src/java/com/healthmarketscience/jackcess/Column.java
  5. 1
    0
      src/java/com/healthmarketscience/jackcess/ColumnBuilder.java
  6. 1
    1
      src/java/com/healthmarketscience/jackcess/Cursor.java
  7. 10
    1
      src/java/com/healthmarketscience/jackcess/DataType.java
  8. 16
    7
      src/java/com/healthmarketscience/jackcess/Database.java
  9. 1
    1
      src/java/com/healthmarketscience/jackcess/ExportUtil.java
  10. 1
    0
      src/java/com/healthmarketscience/jackcess/IndexData.java
  11. 35
    0
      src/java/com/healthmarketscience/jackcess/JetFormat.java
  12. 1
    1
      src/java/com/healthmarketscience/jackcess/RowFilter.java
  13. 98
    27
      src/java/com/healthmarketscience/jackcess/Table.java
  14. 53
    0
      src/java/com/healthmarketscience/jackcess/complex/Attachment.java
  15. 326
    0
      src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java
  16. 400
    0
      src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java
  17. 29
    0
      src/java/com/healthmarketscience/jackcess/complex/ComplexDataType.java
  18. 43
    0
      src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java
  19. 246
    0
      src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java
  20. 143
    0
      src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java
  21. 14
    0
      src/java/com/healthmarketscience/jackcess/complex/SingleValue.java
  22. 63
    0
      src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java
  23. 33
    0
      src/java/com/healthmarketscience/jackcess/complex/Version.java
  24. 212
    0
      src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java
  25. 309
    0
      test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java
  26. 54
    22
      test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java
  27. 2
    5
      test/src/java/com/healthmarketscience/jackcess/ExportTest.java
  28. 1
    1
      test/src/java/com/healthmarketscience/jackcess/TableTest.java

+ 2
- 0
TODO.txt 파일 보기

@@ -1,5 +1,7 @@
Missing pieces:

- re-use memo/ole data in "other page(s)" when updating rows
* MEDIUM
- fix long text index entries
* ???
- implement foreign key index creation

+ 6
- 0
src/changes/changes.xml 파일 보기

@@ -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.

+ 2
- 2
src/java/com/healthmarketscience/jackcess/ByteUtil.java 파일 보기

@@ -544,7 +544,7 @@ public final class ByteUtil {
}

/**
* Returns a copy of the the given array of the given length.
* Returns a copy of the given array of the given length.
*/
public static byte[] copyOf(byte[] arr, int newLength)
{
@@ -552,7 +552,7 @@ public final class ByteUtil {
}

/**
* Returns a copy of the the given array of the given length starting at the
* Returns a copy of the given array of the given length starting at the
* given position.
*/
public static byte[] copyOf(byte[] arr, int offset, int newLength)

+ 130
- 5
src/java/com/healthmarketscience/jackcess/Column.java 파일 보기

@@ -46,11 +46,15 @@ import java.sql.SQLException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.healthmarketscience.jackcess.complex.ComplexColumnInfo;
import com.healthmarketscience.jackcess.complex.ComplexValue;
import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
import com.healthmarketscience.jackcess.scsu.Compress;
import com.healthmarketscience.jackcess.scsu.EndOfInputException;
import com.healthmarketscience.jackcess.scsu.Expand;
@@ -204,6 +208,8 @@ public class Column implements Comparable<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;
}
}

+ 1
- 0
src/java/com/healthmarketscience/jackcess/ColumnBuilder.java 파일 보기

@@ -151,6 +151,7 @@ public class ColumnBuilder {
setScale(template.getScale());
setPrecision(template.getPrecision());
}
setCompressedUnicode(template.isCompressedUnicode());
return this;
}

+ 1
- 1
src/java/com/healthmarketscience/jackcess/Cursor.java 파일 보기

@@ -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);
}


+ 10
- 1
src/java/com/healthmarketscience/jackcess/DataType.java 파일 보기

@@ -147,6 +147,11 @@ public enum DataType {
* MSysAccessObjects table). Handled like a fixed length BINARY/OLE.
*/
UNKNOWN_11((byte) 0x11, null, 3992),
/**
* Complex type corresponds to a special LONG autonumber field which is the
* key for a secondary table which holds the "real" data.
*/
COMPLEX_TYPE((byte) 0x12, null, 4),
/**
* Dummy type for a fixed length type which is not currently supported.
* Handled like a fixed length BINARY.
@@ -410,7 +415,11 @@ public enum DataType {
}

public boolean mayBeAutoNumber() {
return((this == LONG) || (this == GUID));
return((this == LONG) || (this == GUID) || (this == COMPLEX_TYPE));
}

public boolean isMultipleAutoNumberAllowed() {
return (this == COMPLEX_TYPE);
}

public boolean isUnsupported() {

+ 16
- 7
src/java/com/healthmarketscience/jackcess/Database.java 파일 보기

@@ -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);

+ 1
- 1
src/java/com/healthmarketscience/jackcess/ExportUtil.java 파일 보기

@@ -327,7 +327,7 @@ public class ExportUtil {

// fill raw row data in array
for (int i = 0; i < columns.size(); i++) {
unfilteredRowData[i] = row.get(columns.get(i).getName());
unfilteredRowData[i] = columns.get(i).getRowValue(row);
}

// apply filter

+ 1
- 0
src/java/com/healthmarketscience/jackcess/IndexData.java 파일 보기

@@ -1205,6 +1205,7 @@ public abstract class IndexData {
case INT:
case LONG:
case MONEY:
case COMPLEX_TYPE:
return new IntegerColumnDescriptor(col, flags);
case FLOAT:
case DOUBLE:

+ 35
- 0
src/java/com/healthmarketscience/jackcess/JetFormat.java 파일 보기

@@ -182,6 +182,7 @@ public abstract class JetFormat {
public final int OFFSET_NEXT_TABLE_DEF_PAGE;
public final int OFFSET_NUM_ROWS;
public final int OFFSET_NEXT_AUTO_NUMBER;
public final int OFFSET_NEXT_COMPLEX_AUTO_NUMBER;
public final int OFFSET_TABLE_TYPE;
public final int OFFSET_MAX_COLS;
public final int OFFSET_NUM_VAR_COLS;
@@ -201,6 +202,7 @@ public abstract class JetFormat {
public final int OFFSET_COLUMN_SCALE;
public final int OFFSET_COLUMN_SORT_ORDER;
public final int OFFSET_COLUMN_CODE_PAGE;
public final int OFFSET_COLUMN_COMPLEX_ID;
public final int OFFSET_COLUMN_FLAGS;
public final int OFFSET_COLUMN_COMPRESSED_UNICODE;
public final int OFFSET_COLUMN_LENGTH;
@@ -313,6 +315,7 @@ public abstract class JetFormat {
OFFSET_NEXT_TABLE_DEF_PAGE = defineOffsetNextTableDefPage();
OFFSET_NUM_ROWS = defineOffsetNumRows();
OFFSET_NEXT_AUTO_NUMBER = defineOffsetNextAutoNumber();
OFFSET_NEXT_COMPLEX_AUTO_NUMBER = defineOffsetNextComplexAutoNumber();
OFFSET_TABLE_TYPE = defineOffsetTableType();
OFFSET_MAX_COLS = defineOffsetMaxCols();
OFFSET_NUM_VAR_COLS = defineOffsetNumVarCols();
@@ -332,6 +335,7 @@ public abstract class JetFormat {
OFFSET_COLUMN_SCALE = defineOffsetColumnScale();
OFFSET_COLUMN_SORT_ORDER = defineOffsetColumnSortOrder();
OFFSET_COLUMN_CODE_PAGE = defineOffsetColumnCodePage();
OFFSET_COLUMN_COMPLEX_ID = defineOffsetColumnComplexId();
OFFSET_COLUMN_FLAGS = defineOffsetColumnFlags();
OFFSET_COLUMN_COMPRESSED_UNICODE = defineOffsetColumnCompressedUnicode();
OFFSET_COLUMN_LENGTH = defineOffsetColumnLength();
@@ -412,6 +416,7 @@ public abstract class JetFormat {
protected abstract int defineOffsetNextTableDefPage();
protected abstract int defineOffsetNumRows();
protected abstract int defineOffsetNextAutoNumber();
protected abstract int defineOffsetNextComplexAutoNumber();
protected abstract int defineOffsetTableType();
protected abstract int defineOffsetMaxCols();
protected abstract int defineOffsetNumVarCols();
@@ -431,6 +436,7 @@ public abstract class JetFormat {
protected abstract int defineOffsetColumnScale();
protected abstract int defineOffsetColumnSortOrder();
protected abstract int defineOffsetColumnCodePage();
protected abstract int defineOffsetColumnComplexId();
protected abstract int defineOffsetColumnFlags();
protected abstract int defineOffsetColumnCompressedUnicode();
protected abstract int defineOffsetColumnLength();
@@ -490,6 +496,8 @@ public abstract class JetFormat {

protected abstract Map<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 {

+ 1
- 1
src/java/com/healthmarketscience/jackcess/RowFilter.java 파일 보기

@@ -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));
}
};
}

+ 98
- 27
src/java/com/healthmarketscience/jackcess/Table.java 파일 보기

@@ -126,6 +126,8 @@ public class Table
private int _rowCount;
/** last long auto number for the table */
private int _lastLongAutoNumber;
/** last complex type auto number for the table */
private int _lastComplexTypeAutoNumber;
/** page number of the definition of this table */
private final int _tableDefPageNumber;
/** max Number of columns in the table (includes previous deletions) */
@@ -136,6 +138,8 @@ public class Table
private List<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);

+ 53
- 0
src/java/com/healthmarketscience/jackcess/complex/Attachment.java 파일 보기

@@ -0,0 +1,53 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.util.Date;

/**
*
* @author James Ahlborn
*/
public interface Attachment extends ComplexValue
{
public byte[] getFileData();

public void setFileData(byte[] data);

public String getFileName();

public void setFileName(String fileName);
public String getFileUrl();

public void setFileUrl(String fileUrl);
public String getFileType();

public void setFileType(String fileType);
public Date getFileTimeStamp();

public void setFileTimeStamp(Date fileTimeStamp);
public Integer getFileFlags();

public void setFileFlags(Integer fileFlags);
}

+ 326
- 0
src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java 파일 보기

@@ -0,0 +1,326 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import com.healthmarketscience.jackcess.ByteUtil;
import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.Table;


/**
*
* @author James Ahlborn
*/
public class AttachmentColumnInfo extends ComplexColumnInfo<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());
}
}
}

+ 400
- 0
src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java 파일 보기

@@ -0,0 +1,400 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.CursorBuilder;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.Database;
import com.healthmarketscience.jackcess.IndexCursor;
import com.healthmarketscience.jackcess.JetFormat;
import com.healthmarketscience.jackcess.PageChannel;
import com.healthmarketscience.jackcess.Table;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* Base class for the additional information tracked for complex columns.
*
* @author James Ahlborn
*/
public abstract class ComplexColumnInfo<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)));
}
}
}

+ 29
- 0
src/java/com/healthmarketscience/jackcess/complex/ComplexDataType.java 파일 보기

@@ -0,0 +1,29 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

/**
*
* @author James Ahlborn
*/
public enum ComplexDataType
{
ATTACHMENT, MULTI_VALUE, VERSION_HISTORY, UNSUPPORTED;
}

+ 43
- 0
src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java 파일 보기

@@ -0,0 +1,43 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.io.IOException;

import com.healthmarketscience.jackcess.Column;

/**
*
* @author James Ahlborn
*/
public interface ComplexValue
{
public int getId();

public void setId(int newId);
public ComplexValueForeignKey getComplexValueForeignKey();

public void setComplexValueForeignKey(ComplexValueForeignKey complexValueFk);

public Column getColumn();

public void update() throws IOException;
}

+ 246
- 0
src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java 파일 보기

@@ -0,0 +1,246 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.io.IOException;
import java.io.ObjectStreamException;
import java.util.Date;
import java.util.List;
import java.util.Map;

import com.healthmarketscience.jackcess.Column;

/**
*
* @author James Ahlborn
*/
public class ComplexValueForeignKey extends Number
{
private static final long serialVersionUID = 20110805L;
private final Column _column;
private final int _value;
private List<? 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);
}
}

+ 143
- 0
src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java 파일 보기

@@ -0,0 +1,143 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.Table;

/**
*
* @author James Ahlborn
*/
public class MultiValueColumnInfo extends ComplexColumnInfo<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();
}
}
}

+ 14
- 0
src/java/com/healthmarketscience/jackcess/complex/SingleValue.java 파일 보기

@@ -0,0 +1,14 @@
// Copyright (c) 2011 Boomi, Inc.

package com.healthmarketscience.jackcess.complex;

/**
*
* @author James Ahlborn
*/
public interface SingleValue extends ComplexValue
{
public Object get();

public void set(Object value);
}

+ 63
- 0
src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java 파일 보기

@@ -0,0 +1,63 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.Table;

/**
*
* @author James Ahlborn
*/
public class UnsupportedColumnInfo extends ComplexColumnInfo<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;
}
}

+ 33
- 0
src/java/com/healthmarketscience/jackcess/complex/Version.java 파일 보기

@@ -0,0 +1,33 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.util.Date;

/**
*
* @author James Ahlborn
*/
public interface Version extends ComplexValue, Comparable<Version>
{
public String getValue();

public Date getModifiedDate();
}

+ 212
- 0
src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java 파일 보기

@@ -0,0 +1,212 @@
/*
Copyright (c) 2011 James Ahlborn

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/

package com.healthmarketscience.jackcess.complex;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.Table;

/**
*
* @author James Ahlborn
*/
public class VersionHistoryColumnInfo extends ComplexColumnInfo<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();
}
}
}

+ 309
- 0
test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java 파일 보기

@@ -0,0 +1,309 @@
// Copyright (c) 2011 Boomi, Inc.

package com.healthmarketscience.jackcess;

import java.io.File;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;

import static com.healthmarketscience.jackcess.DatabaseTest.*;
import com.healthmarketscience.jackcess.complex.Attachment;
import com.healthmarketscience.jackcess.complex.ComplexDataType;
import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
import com.healthmarketscience.jackcess.complex.SingleValue;
import com.healthmarketscience.jackcess.complex.Version;
import junit.framework.TestCase;


/**
*
* @author James Ahlborn
*/
public class ComplexColumnTest extends TestCase
{

public ComplexColumnTest(String name) {
super(name);
}

public void testVersions() throws Exception
{
Database db = DatabaseTest.openCopy(Database.FileFormat.V2007, new File("/data2/jackcess_test/complexDataTest.accdb"));
db.setTimeZone(TEST_TZ);

Table t1 = db.getTable("Table1");
Column col = t1.getColumn("append-memo-data");
assertTrue(col.isAppendOnly());
Column verCol = col.getVersionHistoryColumn();
assertNotNull(verCol);
assertEquals(ComplexDataType.VERSION_HISTORY,
verCol.getComplexInfo().getType());

for(Map<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)
};
}

+ 54
- 22
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java 파일 보기

@@ -51,11 +51,13 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.UUID;

import static com.healthmarketscience.jackcess.Database.*;
import static com.healthmarketscience.jackcess.JetFormatTest.*;
import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
import junit.framework.TestCase;

/**
@@ -63,6 +65,9 @@ import junit.framework.TestCase;
*/
public class DatabaseTest extends TestCase {

public static final TimeZone TEST_TZ =
TimeZone.getTimeZone("America/New_York");
static boolean _autoSync = Database.DEFAULT_AUTO_SYNC;


@@ -972,22 +977,7 @@ public class DatabaseTest extends TestCase {
for(int i = 0; i < dates.size(); ++i) {
Date expected = dates.get(i);
Date found = foundDates.get(i);
if(expected == null) {
assertNull(found);
} else {
// there are some rounding issues due to dates being stored as
// doubles, but it results in a 1 millisecond difference, so i'm not
// going to worry about it
long expTime = expected.getTime();
long foundTime = found.getTime();
try {
assertTrue((expTime == foundTime) ||
(Math.abs(expTime - foundTime) <= 1));
} catch(Error e) {
System.err.println("Expected " + expTime + ", found " + foundTime);
throw e;
}
}
assertSameDate(expected, found);
}
}
}
@@ -1253,18 +1243,31 @@ public class DatabaseTest extends TestCase {
}
static void dumpDatabase(Database mdb) throws Exception {
dumpDatabase(mdb, new PrintWriter(System.out, true));
dumpDatabase(mdb, false);
}

static void dumpDatabase(Database mdb, boolean systemTables)
throws Exception
{
dumpDatabase(mdb, systemTables, new PrintWriter(System.out, true));
}

static void dumpTable(Table table) throws Exception {
dumpTable(table, new PrintWriter(System.out, true));
}

static void dumpDatabase(Database mdb, PrintWriter writer) throws Exception {
static void dumpDatabase(Database mdb, boolean systemTables,
PrintWriter writer) throws Exception
{
writer.println("DATABASE:");
for(Table table : mdb) {
dumpTable(table, writer);
}
if(systemTables) {
for(String sysTableName : mdb.getSystemTableNames()) {
dumpTable(mdb.getSystemTable(sysTableName), writer);
}
}
}

static void dumpTable(Table table, PrintWriter writer) throws Exception {
@@ -1280,18 +1283,28 @@ public class DatabaseTest extends TestCase {
}
writer.println("COLUMNS: " + colNames);
for(Map<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
{

+ 2
- 5
test/src/java/com/healthmarketscience/jackcess/ExportTest.java 파일 보기

@@ -31,7 +31,6 @@ import java.io.BufferedWriter;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;

import junit.framework.TestCase;
import org.apache.commons.lang.SystemUtils;
@@ -55,14 +54,12 @@ public class ExportTest extends TestCase

public void testExportToFile() throws Exception
{
TimeZone testTZ = TimeZone.getTimeZone("America/New_York");

DateFormat df = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
df.setTimeZone(testTZ);
df.setTimeZone(TEST_TZ);

for (final FileFormat fileFormat : JetFormatTest.SUPPORTED_FILEFORMATS) {
Database db = create(fileFormat);
db.setTimeZone(testTZ);
db.setTimeZone(TEST_TZ);

Table t = new TableBuilder("test")
.addColumn(new ColumnBuilder("col1", DataType.TEXT))

+ 1
- 1
test/src/java/com/healthmarketscience/jackcess/TableTest.java 파일 보기

@@ -121,7 +121,7 @@ public class TableTest extends TestCase {
{
return _testTable.createRow(
row, _testTable.getFormat().MAX_ROW_SIZE,
_testTable.getPageChannel().createPageBuffer(), false, 0);
_testTable.getPageChannel().createPageBuffer(), 0);
}

private ByteBuffer[] encodeColumns(Object... row)

Loading…
취소
저장