diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2013-06-18 02:44:20 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2013-06-18 02:44:20 +0000 |
commit | b8905cf9ecec87f760c923eb972868e070290e7d (patch) | |
tree | 2fe10d622cdbecac97a161e15436d0d3012b481c | |
parent | 08a343c1e2072eebf97288a13bdd75c8885ebd42 (diff) | |
download | jackcess-b8905cf9ecec87f760c923eb972868e070290e7d.tar.gz jackcess-b8905cf9ecec87f760c923eb972868e070290e7d.zip |
merge trunk changes through r738
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jackcess-2@739 f203690c-595d-4dc9-a70b-905162fa7fd2
17 files changed, 517 insertions, 65 deletions
@@ -82,6 +82,12 @@ read-only support</role> </roles> </contributor> + <contributor> + <name>Lorenzo Carrara</name> + <roles> + <role>Reverse engineered the attachment data encoding.</role> + </roles> + </contributor> </contributors> <issueManagement> <system>SourceForge2</system> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 260f266..a3e5c6f 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -9,6 +9,12 @@ Fix partial page updates when using CodecHandlers which can only do full page encoding. </action> + <action dev="jahlborn" type="update"> + Add more methods to Database for retrieving Relationships. + </action> + <action dev="jahlborn" type="update"> + Implement attachment decoding, thanks to Lorenzo Carrara. + </action> </release> <release version="1.2.12" date="2013-05-09"> <action dev="jahlborn" type="fix" system="SourceForge2" issue="94"> diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index 0044f26..71a6d59 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -188,6 +188,32 @@ public interface Database extends Iterable<Table>, Closeable, Flushable throws IOException; /** + * Finds all the relationships in the database for the given table. + * @usage _intermediate_method_ + */ + public List<Relationship> getRelationships(Table table) throws IOException; + + /** + * Finds all the relationships in the database in <i>non-system</i> tables. + * </p> + * Warning, this may load <i>all</i> the Tables (metadata, not data) in the + * database which could cause memory issues. + * @usage _intermediate_method_ + */ + public List<Relationship> getRelationships() throws IOException; + + /** + * Finds <i>all</i> the relationships in the database, <i>including system + * tables</i>. + * </p> + * Warning, this may load <i>all</i> the Tables (metadata, not data) in the + * database which could cause memory issues. + * @usage _intermediate_method_ + */ + public List<Relationship> getSystemRelationships() + throws IOException; + + /** * Finds all the queries in the database. * @usage _intermediate_method_ */ diff --git a/src/java/com/healthmarketscience/jackcess/complex/Attachment.java b/src/java/com/healthmarketscience/jackcess/complex/Attachment.java index 2f4b046..c569315 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/Attachment.java +++ b/src/java/com/healthmarketscience/jackcess/complex/Attachment.java @@ -19,6 +19,7 @@ USA package com.healthmarketscience.jackcess.complex; +import java.io.IOException; import java.util.Date; /** @@ -28,10 +29,14 @@ import java.util.Date; */ public interface Attachment extends ComplexValue { - public byte[] getFileData(); + public byte[] getFileData() throws IOException; public void setFileData(byte[] data); + public byte[] getEncodedFileData() throws IOException; + + public void setEncodedFileData(byte[] data); + public String getFileName(); public void setFileName(String fileName); diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java index 1b651cd..aeff8c9 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java @@ -95,11 +95,9 @@ public abstract class ComplexValueForeignKey extends Number } @Override - public String toString() - { + public String toString() { return String.valueOf(get()); - } - + } public abstract int get(); @@ -138,6 +136,14 @@ public abstract class ComplexValueForeignKey extends Number Date timeStamp, Integer flags) throws IOException; + public abstract Attachment addEncodedAttachment(byte[] encodedData) + throws IOException; + + public abstract Attachment addEncodedAttachment( + String url, String name, String type, byte[] encodedData, + Date timeStamp, Integer flags) + throws IOException; + public abstract Attachment updateAttachment(Attachment attachment) throws IOException; diff --git a/src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java b/src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java index ad2c1fe..857c631 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java +++ b/src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java @@ -29,6 +29,7 @@ package com.healthmarketscience.jackcess.impl; import java.io.FileWriter; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintWriter; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -609,7 +610,7 @@ public final class ByteUtil { * Utility byte stream similar to ByteArrayOutputStream but with extended * accessibility to the bytes. */ - public static class ByteStream + public static class ByteStream extends OutputStream { private byte[] _bytes; private int _length; @@ -641,15 +642,18 @@ public final class ByteUtil { } } + @Override public void write(int b) { ensureNewCapacity(1); _bytes[_length++] = (byte)b; } + @Override public void write(byte[] b) { write(b, 0, b.length); } + @Override public void write(byte[] b, int offset, int length) { ensureNewCapacity(length); System.arraycopy(b, offset, _bytes, _length, length); @@ -671,6 +675,11 @@ public final class ByteUtil { Arrays.fill(_bytes, oldLength, _length, b); } + public void skip(int n) { + ensureNewCapacity(n); + _length += n; + } + public void writeTo(ByteStream out) { out.write(_bytes, 0, _length); } diff --git a/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 465f9ea..154d939 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -638,8 +638,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { private byte[] readLongValue(byte[] lvalDefinition) throws IOException { - ByteBuffer def = ByteBuffer.wrap(lvalDefinition) - .order(PageChannel.DEFAULT_BYTE_ORDER); + ByteBuffer def = PageChannel.wrap(lvalDefinition); int lengthWithFlags = def.getInt(); int length = lengthWithFlags & (~LONG_VALUE_TYPE_MASK); diff --git a/src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index dafa0c5..87f7ef1 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -59,6 +59,7 @@ import java.util.TreeSet; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.Cursor; +import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.IndexBuilder; @@ -68,8 +69,8 @@ import com.healthmarketscience.jackcess.Relationship; import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.RuntimeIOException; import com.healthmarketscience.jackcess.Table; -import com.healthmarketscience.jackcess.query.Query; import com.healthmarketscience.jackcess.impl.query.QueryImpl; +import com.healthmarketscience.jackcess.query.Query; import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher; import com.healthmarketscience.jackcess.util.ErrorHandler; import com.healthmarketscience.jackcess.util.LinkResolver; @@ -996,14 +997,6 @@ public class DatabaseImpl implements Database TableImpl table1, TableImpl table2) throws IOException { - // the relationships table does not get loaded until first accessed - if(_relationships == null) { - _relationships = getSystemTable(TABLE_SYSTEM_RELATIONSHIPS); - if(_relationships == null) { - throw new IOException("Could not find system relationships table"); - } - } - int nameCmp = table1.getName().compareTo(table2.getName()); if(nameCmp == 0) { throw new IllegalArgumentException("Must provide two different tables"); @@ -1017,14 +1010,59 @@ public class DatabaseImpl implements Database table2 = tmp; } + return getRelationshipsImpl(table1, table2, true); + } + + public List<Relationship> getRelationships(Table table) + throws IOException + { + if(table == null) { + throw new IllegalArgumentException("Must provide a table"); + } + // since we are getting relationships specific to certain table include + // all tables + return getRelationshipsImpl((TableImpl)table, null, true); + } + + public List<Relationship> getRelationships() + throws IOException + { + return getRelationshipsImpl(null, null, false); + } + + public List<Relationship> getSystemRelationships() + throws IOException + { + return getRelationshipsImpl(null, null, true); + } + + private List<Relationship> getRelationshipsImpl( + TableImpl table1, TableImpl table2, boolean includeSystemTables) + throws IOException + { + // the relationships table does not get loaded until first accessed + if(_relationships == null) { + _relationships = getSystemTable(TABLE_SYSTEM_RELATIONSHIPS); + if(_relationships == null) { + throw new IOException("Could not find system relationships table"); + } + } List<Relationship> relationships = new ArrayList<Relationship>(); + + if(table1 != null) { Cursor cursor = createCursorWithOptionalIndex( _relationships, REL_COL_FROM_TABLE, table1.getName()); - collectRelationships(cursor, table1, table2, relationships); + collectRelationships(cursor, table1, table2, relationships, + includeSystemTables); cursor = createCursorWithOptionalIndex( _relationships, REL_COL_TO_TABLE, table1.getName()); - collectRelationships(cursor, table2, table1, relationships); + collectRelationships(cursor, table2, table1, relationships, + includeSystemTables); + } else { + collectRelationships(new CursorBuilder(_relationships).toCursor(), + null, null, relationships, includeSystemTables); + } return relationships; } @@ -1198,17 +1236,22 @@ public class DatabaseImpl implements Database * Finds the relationships matching the given from and to tables from the * given cursor and adds them to the given list. */ - private static void collectRelationships( + private void collectRelationships( Cursor cursor, TableImpl fromTable, TableImpl toTable, - List<Relationship> relationships) + List<Relationship> relationships, boolean includeSystemTables) + throws IOException { + String fromTableName = ((fromTable != null) ? fromTable.getName() : null); + String toTableName = ((toTable != null) ? toTable.getName() : null); + for(Row row : cursor) { String fromName = (String)row.get(REL_COL_FROM_TABLE); String toName = (String)row.get(REL_COL_TO_TABLE); - if(fromTable.getName().equalsIgnoreCase(fromName) && - toTable.getName().equalsIgnoreCase(toName)) - { + if(((fromTableName == null) || + fromTableName.equalsIgnoreCase(fromName)) && + ((toTableName == null) || + toTableName.equalsIgnoreCase(toName))) { String relName = (String)row.get(REL_COL_NAME); @@ -1222,20 +1265,37 @@ public class DatabaseImpl implements Database } } + TableImpl relFromTable = fromTable; + if(relFromTable == null) { + relFromTable = getTable(fromName, includeSystemTables); + if(relFromTable == null) { + // invalid table or ignoring system tables, just ignore + continue; + } + } + TableImpl relToTable = toTable; + if(relToTable == null) { + relToTable = getTable(toName, includeSystemTables); + if(relToTable == null) { + // invalid table or ignoring system tables, just ignore + continue; + } + } + if(rel == null) { // new relationship int numCols = (Integer)row.get(REL_COL_COLUMN_COUNT); int flags = (Integer)row.get(REL_COL_FLAGS); - rel = new RelationshipImpl(relName, fromTable, toTable, + rel = new RelationshipImpl(relName, relFromTable, relToTable, flags, numCols); relationships.add(rel); } // add column info int colIdx = (Integer)row.get(REL_COL_COLUMN_INDEX); - ColumnImpl fromCol = fromTable.getColumn( + ColumnImpl fromCol = relFromTable.getColumn( (String)row.get(REL_COL_FROM_COLUMN)); - ColumnImpl toCol = toTable.getColumn( + ColumnImpl toCol = relToTable.getColumn( (String)row.get(REL_COL_TO_COLUMN)); rel.getFromColumns().set(colIdx, fromCol); @@ -1612,8 +1672,7 @@ public class DatabaseImpl implements Database double dateVal = Double.longBitsToDouble(buffer.getLong()); byte[] pwdMask = new byte[4]; - ByteBuffer.wrap(pwdMask).order(PageChannel.DEFAULT_BYTE_ORDER) - .putInt((int)dateVal); + PageChannel.wrap(pwdMask).putInt((int)dateVal); return pwdMask; } @@ -1749,7 +1808,7 @@ public class DatabaseImpl implements Database } public Row getObjectRow(Integer parentId, String name, - Collection<String> columns) + Collection<String> columns) throws IOException { Cursor cur = findRow(parentId, name); diff --git a/src/java/com/healthmarketscience/jackcess/impl/PageChannel.java b/src/java/com/healthmarketscience/jackcess/impl/PageChannel.java index d3d5367..2ea9d27 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/PageChannel.java +++ b/src/java/com/healthmarketscience/jackcess/impl/PageChannel.java @@ -419,4 +419,12 @@ public class PageChannel implements Channel, Flushable { .position(position) .mark(); } + + /** + * Returns a ByteBuffer wrapping the given bytes and configured with the + * default byte order. + */ + public static ByteBuffer wrap(byte[] bytes) { + return ByteBuffer.wrap(bytes).order(DEFAULT_BYTE_ORDER); +} } diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java index c0e5646..69c43df 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java @@ -19,8 +19,19 @@ USA package com.healthmarketscience.jackcess.impl.complex; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.Row; @@ -31,6 +42,9 @@ import com.healthmarketscience.jackcess.complex.ComplexDataType; import com.healthmarketscience.jackcess.complex.ComplexValue; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; import com.healthmarketscience.jackcess.impl.ByteUtil; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.JetFormat; +import com.healthmarketscience.jackcess.impl.PageChannel; /** @@ -41,9 +55,21 @@ import com.healthmarketscience.jackcess.impl.ByteUtil; public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> implements AttachmentColumnInfo { + /** some file formats which may not be worth re-compressing */ + private static final Set<String> COMPRESSED_FORMATS = new HashSet<String>( + Arrays.asList("jpg", "zip", "gz", "bz2", "z", "7z", "cab", "rar", + "mp3", "mpg")); + private static final String FILE_NAME_COL_NAME = "FileName"; private static final String FILE_TYPE_COL_NAME = "FileType"; + private static final int DATA_TYPE_RAW = 0; + private static final int DATA_TYPE_COMPRESSED = 1; + + private static final int UNKNOWN_HEADER_VAL = 1; + private static final int WRAPPER_HEADER_SIZE = 8; + private static final int CONTENT_HEADER_SIZE = 12; + private final Column _fileUrlCol; private final Column _fileNameCol; private final Column _fileTypeCol; @@ -146,19 +172,21 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> Date ts = (Date)getFileTimeStampColumn().getRowValue(rawValue); byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue); - return new AttachmentImpl(id, complexValueFk, url, name, type, data, - ts, flags); + return new AttachmentImpl(id, complexValueFk, url, name, type, null, + ts, flags, data); } @Override - protected Object[] asRow(Object[] row, Attachment attachment) { + protected Object[] asRow(Object[] row, Attachment attachment) + throws IOException + { 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()); + getFileDataColumn().setRowValue(row, attachment.getEncodedFileData()); return row; } @@ -184,9 +212,35 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> String type, byte[] data, Date timeStamp, Integer flags) { return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, - data, timeStamp, flags); + data, timeStamp, flags, null); } + public static Attachment newEncodedAttachment(byte[] encodedData) { + return newEncodedAttachment(INVALID_FK, encodedData); + } + + public static Attachment newEncodedAttachment( + ComplexValueForeignKey complexValueFk, byte[] encodedData) { + return newEncodedAttachment(complexValueFk, null, null, null, encodedData, + null, null); + } + + public static Attachment newEncodedAttachment( + String url, String name, String type, byte[] encodedData, + Date timeStamp, Integer flags) + { + return newEncodedAttachment(INVALID_FK, url, name, type, + encodedData, timeStamp, flags); + } + + public static Attachment newEncodedAttachment( + ComplexValueForeignKey complexValueFk, String url, String name, + String type, byte[] encodedData, Date timeStamp, Integer flags) + { + return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, + null, timeStamp, flags, encodedData); + } + private static class AttachmentImpl extends ComplexValueImpl implements Attachment @@ -197,10 +251,11 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> private byte[] _data; private Date _timeStamp; private Integer _flags; + private byte[] _encodedData; private AttachmentImpl(Id id, ComplexValueForeignKey complexValueFk, String url, String name, String type, byte[] data, - Date timeStamp, Integer flags) + Date timeStamp, Integer flags, byte[] encodedData) { super(id, complexValueFk); _url = url; @@ -209,14 +264,31 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> _data = data; _timeStamp = timeStamp; _flags = flags; + _encodedData = encodedData; } - public byte[] getFileData() { + public byte[] getFileData() throws IOException { + if((_data == null) && (_encodedData != null)) { + _data = decodeData(); + } return _data; } public void setFileData(byte[] data) { _data = data; + _encodedData = null; + } + + public byte[] getEncodedFileData() throws IOException { + if((_encodedData == null) && (_data != null)) { + _encodedData = encodeData(); + } + return _encodedData; + } + + public void setEncodedFileData(byte[] data) { + _encodedData = data; + _data = null; } public String getFileName() { @@ -268,13 +340,143 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> } @Override - public String toString() - { + public String toString() { + + String dataStr = null; + try { + dataStr = ByteUtil.toHexString(getFileData()); + } catch(IOException e) { + dataStr = e.toString(); + } + return "Attachment(" + getComplexValueForeignKey() + "," + getId() + ") " + getFileUrl() + ", " + getFileName() + ", " + getFileType() + ", " + getFileTimeStamp() + ", " + getFileFlags() + ", " + - ByteUtil.toHexString(getFileData()); + dataStr; } + + /** + * Decodes the raw attachment file data to get the _actual_ content. + */ + private byte[] decodeData() throws IOException { + + if(_encodedData.length < WRAPPER_HEADER_SIZE) { + // nothing we can do + throw new IOException("Unknown encoded attachment data format"); } + // read initial header info + ByteBuffer bb = PageChannel.wrap(_encodedData); + int typeFlag = bb.getInt(); + int dataLen = bb.getInt(); + + DataInputStream contentStream = null; + try { + InputStream bin = new ByteArrayInputStream( + _encodedData, WRAPPER_HEADER_SIZE, + _encodedData.length - WRAPPER_HEADER_SIZE); + + if(typeFlag == DATA_TYPE_RAW) { + // nothing else to do + } else if(typeFlag == DATA_TYPE_COMPRESSED) { + // actual content is deflate compressed + bin = new InflaterInputStream(bin); + } else { + throw new IOException( + "Unknown encoded attachment data type " + typeFlag); +} + + contentStream = new DataInputStream(bin); + + // header is an unknown flag followed by the "file extension" of the + // data (no clue why we need that again since it's already a separate + // field in the attachment table). just skip all of it + byte[] tmpBytes = new byte[4]; + contentStream.readFully(tmpBytes); + int headerLen = PageChannel.wrap(tmpBytes).getInt(); + contentStream.skipBytes(headerLen - 4); + + // calculate actual data length and read it (note, header length + // includes the bytes for the length) + tmpBytes = new byte[dataLen - headerLen]; + contentStream.readFully(tmpBytes); + + return tmpBytes; + + } finally { + if(contentStream != null) { + try { + contentStream.close(); + } catch(IOException e) { + // ignored + } + } + } + } + + /** + * Encodes the actual attachment file data to get the raw, stored format. + */ + private byte[] encodeData() throws IOException { + + // possibly compress data based on file type + String type = ((_type != null) ? _type.toLowerCase() : ""); + boolean shouldCompress = !COMPRESSED_FORMATS.contains(type); + + // encode extension, which ends w/ a null byte + type += '\0'; + ByteBuffer typeBytes = ColumnImpl.encodeUncompressedText( + type, JetFormat.VERSION_12.CHARSET); + int headerLen = typeBytes.remaining() + CONTENT_HEADER_SIZE; + + int dataLen = _data.length; + ByteUtil.ByteStream dataStream = new ByteUtil.ByteStream( + WRAPPER_HEADER_SIZE + headerLen + dataLen); + + // write the wrapper header info + ByteBuffer bb = PageChannel.wrap(dataStream.getBytes()); + bb.putInt(shouldCompress ? DATA_TYPE_COMPRESSED : DATA_TYPE_RAW); + bb.putInt(dataLen + headerLen); + dataStream.skip(WRAPPER_HEADER_SIZE); + + OutputStream contentStream = dataStream; + Deflater deflater = null; + try { + + if(shouldCompress) { + contentStream = new DeflaterOutputStream( + contentStream, deflater = new Deflater(3)); + } + + // write the header w/ the file extension + byte[] tmpBytes = new byte[CONTENT_HEADER_SIZE]; + PageChannel.wrap(tmpBytes) + .putInt(headerLen) + .putInt(UNKNOWN_HEADER_VAL) + .putInt(type.length()); + contentStream.write(tmpBytes); + contentStream.write(typeBytes.array(), 0, typeBytes.remaining()); + + // write the _actual_ contents + contentStream.write(_data); + contentStream.close(); + contentStream = null; + + return dataStream.toByteArray(); + + } finally { + if(contentStream != null) { + try { + contentStream.close(); + } catch(IOException e) { + // ignored + } + } + if(deflater != null) { + deflater.end(); + } + } + } + } + } diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java index 9773496..83e86a2 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java @@ -28,7 +28,6 @@ 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; @@ -283,8 +282,10 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> ((TableImpl)_flatTable).getRowId(row)); } - protected Object[] asRow(Object[] row, V value) { - ComplexValue.Id id = value.getId(); + protected Object[] asRow(Object[] row, V value) + throws IOException + { + ComplexValue.Id id = value.getId(); _pkCol.setRowValue( row, ((id != INVALID_ID) ? id : Column.AUTO_NUMBER)); ComplexValueForeignKey cFk = value.getComplexValueForeignKey(); diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java index d196ae5..4e0cc0c 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java @@ -71,6 +71,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return _value; } + @Override public Column getColumn() { return _column; } @@ -100,6 +101,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return (UnsupportedColumnInfo)getComplexInfo(); } + @Override public int countValues() throws IOException { return getComplexInfo().countValues(get()); } @@ -108,6 +110,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return getComplexInfo().getRawValues(get()); } + @Override public List<? extends ComplexValue> getValues() throws IOException { if(_values == null) { _values = getComplexInfo().getValues(this); @@ -115,6 +118,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return _values; } + @Override @SuppressWarnings("unchecked") public List<Version> getVersions() throws IOException { if(getComplexType() != ComplexDataType.VERSION_HISTORY) { @@ -123,6 +127,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return (List<Version>)getValues(); } + @Override @SuppressWarnings("unchecked") public List<Attachment> getAttachments() throws IOException { if(getComplexType() != ComplexDataType.ATTACHMENT) { @@ -131,6 +136,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return (List<Attachment>)getValues(); } + @Override @SuppressWarnings("unchecked") public List<SingleValue> getMultiValues() throws IOException { if(getComplexType() != ComplexDataType.MULTI_VALUE) { @@ -139,6 +145,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return (List<SingleValue>)getValues(); } + @Override @SuppressWarnings("unchecked") public List<UnsupportedValue> getUnsupportedValues() throws IOException { if(getComplexType() != ComplexDataType.UNSUPPORTED) { @@ -147,15 +154,18 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return (List<UnsupportedValue>)getValues(); } + @Override public void reset() { // discard any cached values _values = null; } + @Override public Version addVersion(String value) throws IOException { return addVersion(value, new Date()); } + @Override public Version addVersion(String value, Date modifiedDate) throws IOException { reset(); Version v = VersionHistoryColumnInfoImpl.newVersion(this, value, modifiedDate); @@ -163,10 +173,12 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return v; } + @Override public Attachment addAttachment(byte[] data) throws IOException { return addAttachment(null, null, null, data, null, null); } + @Override public Attachment addAttachment( String url, String name, String type, byte[] data, Date timeStamp, Integer flags) @@ -179,18 +191,41 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return a; } + @Override + public Attachment addEncodedAttachment(byte[] encodedData) + throws IOException + { + return addEncodedAttachment(null, null, null, encodedData, null, null); + } + + @Override + public Attachment addEncodedAttachment( + String url, String name, String type, byte[] encodedData, + Date timeStamp, Integer flags) + throws IOException + { + reset(); + Attachment a = AttachmentColumnInfoImpl.newEncodedAttachment( + this, url, name, type, encodedData, timeStamp, flags); + getAttachmentInfo().addValue(a); + return a; + } + + @Override public Attachment updateAttachment(Attachment attachment) throws IOException { reset(); getAttachmentInfo().updateValue(attachment); return attachment; } + @Override public Attachment deleteAttachment(Attachment attachment) throws IOException { reset(); getAttachmentInfo().deleteValue(attachment); return attachment; } + @Override public SingleValue addMultiValue(Object value) throws IOException { reset(); SingleValue v = MultiValueColumnInfoImpl.newSingleValue(this, value); @@ -198,18 +233,21 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return v; } + @Override public SingleValue updateMultiValue(SingleValue value) throws IOException { reset(); getMultiValueInfo().updateValue(value); return value; } + @Override public SingleValue deleteMultiValue(SingleValue value) throws IOException { reset(); getMultiValueInfo().deleteValue(value); return value; } + @Override public UnsupportedValue addUnsupportedValue(Map<String,?> values) throws IOException { @@ -219,6 +257,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return v; } + @Override public UnsupportedValue updateUnsupportedValue(UnsupportedValue value) throws IOException { @@ -227,6 +266,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return value; } + @Override public UnsupportedValue deleteUnsupportedValue(UnsupportedValue value) throws IOException { @@ -235,6 +275,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey return value; } + @Override public void deleteAllValues() throws IOException { reset(); getComplexInfo().deleteAllValues(this); diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java index 16b84e3..5f33688 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java @@ -71,7 +71,7 @@ public class MultiValueColumnInfoImpl extends ComplexColumnInfoImpl<SingleValue> } @Override - protected Object[] asRow(Object[] row, SingleValue value) { + protected Object[] asRow(Object[] row, SingleValue value) throws IOException { super.asRow(row, value); getValueColumn().setRowValue(row, value.get()); return row; diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java index 84dbcdb..d84f050 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java @@ -76,7 +76,9 @@ public class UnsupportedColumnInfoImpl } @Override - protected Object[] asRow(Object[] row, UnsupportedValue value) { + protected Object[] asRow(Object[] row, UnsupportedValue value) + throws IOException + { super.asRow(row, value); Map<String,Object> values = value.getValues(); diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java index c1ebbfd..c08d1f1 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java @@ -142,7 +142,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> } @Override - protected Object[] asRow(Object[] row, Version version) { + protected Object[] asRow(Object[] row, Version version) throws IOException { super.asRow(row, version); getValueColumn().setRowValue(row, version.getValue()); getModifiedDateColumn().setRowValue(row, version.getModifiedDate()); diff --git a/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java b/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java index a33b64a..173a53c 100644 --- a/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java @@ -19,21 +19,24 @@ USA package com.healthmarketscience.jackcess; +import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; 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.UnsupportedValue; import com.healthmarketscience.jackcess.complex.Version; -import junit.framework.TestCase; +import com.healthmarketscience.jackcess.impl.ByteUtil; import com.healthmarketscience.jackcess.impl.ColumnImpl; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.PageChannel; +import junit.framework.TestCase; /** @@ -190,6 +193,11 @@ public class ComplexColumnTest extends TestCase row8ValFk.addAttachment(null, "test_data.txt", "txt", getFileBytes("test_data.txt"), null, null); checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt"); + row8ValFk.addEncodedAttachment(null, "test_data2.txt", "txt", + getEncodedFileBytes("test_data2.txt"), null, + null); + checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt", + "test_data2.txt"); Cursor cursor = CursorBuilder.createCursor(t1); assertTrue(cursor.findFirstRow(t1.getColumn("id"), "row4")); @@ -200,17 +208,22 @@ public class ComplexColumnTest extends TestCase 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.setFileType("zip"); + a.setFileName("some_data.zip"); + byte[] newBytes = "this is not a zip file".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()); + assertEquals("zip", updated.getFileType()); + assertEquals("some_data.zip", updated.getFileName()); assertTrue(Arrays.equals(newBytes, updated.getFileData())); + byte[] encBytes = updated.getEncodedFileData(); + assertEquals(newBytes.length + 28, encBytes.length); + ByteBuffer bb = PageChannel.wrap(encBytes); + assertEquals(0, bb.getInt()); + assertTrue(ByteUtil.matchesRange(bb, 28, newBytes)); updated.delete(); checkAttachments(4, row4ValFk, "test_data2.txt"); @@ -368,11 +381,12 @@ public class ComplexColumnTest extends TestCase 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())); + assertTrue(Arrays.equals(getFileBytes(fname), a.getFileData())); + assertTrue(Arrays.equals(getEncodedFileBytes(fname), + a.getEncodedFileData())); } } } @@ -431,17 +445,41 @@ public class ComplexColumnTest extends TestCase throw new RuntimeException("unexpected bytes"); } + private static byte[] getEncodedFileBytes(String fname) throws Exception + { + if("test_data.txt".equals(fname)) { + return TEST_ENC_BYTES; + } + if("test_data2.txt".equals(fname)) { + return TEST2_ENC_BYTES; + } + throw new RuntimeException("unexpected bytes"); + } + private static byte b(int i) { return (byte)i; } - private static final byte[] TEST_BYTES = new byte[] { + private static byte[] getAsciiBytes(String str) { + try { + return str.getBytes("US-ASCII"); + } catch(Exception e) { + throw new RuntimeException(e); + } + } + + private static final byte[] TEST_ENC_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[] TEST_BYTES = getAsciiBytes("this is some test data for attachment."); - private static final byte[] TEST2_BYTES = new byte[] { + private static final byte[] TEST2_ENC_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) }; + + private static final byte[] TEST2_BYTES = getAsciiBytes("this is some more test data for attachment."); + } diff --git a/test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java b/test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java index c43ef9f..e2162c3 100644 --- a/test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java @@ -27,25 +27,33 @@ King of Prussia, PA 19406 package com.healthmarketscience.jackcess; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.List; -import junit.framework.TestCase; - import static com.healthmarketscience.jackcess.DatabaseTest.*; import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; import com.healthmarketscience.jackcess.impl.RelationshipImpl; +import junit.framework.TestCase; /** * @author James Ahlborn */ public class RelationshipTest extends TestCase { + private static final Comparator<Relationship> REL_COMP = new Comparator<Relationship>() { + public int compare(Relationship r1, Relationship r2) { + return String.CASE_INSENSITIVE_ORDER.compare(r1.getName(), r2.getName()); + } + }; + public RelationshipTest(String name) throws Exception { super(name); } - public void testSimple() throws Exception { + public void testTwoTables() throws Exception { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) { Database db = open(testDB); Table t1 = db.getTable("Table1"); @@ -65,11 +73,11 @@ public class RelationshipTest extends TestCase { assertTrue(rel.hasReferentialIntegrity()); assertEquals(4096, ((RelationshipImpl)rel).getFlags()); assertTrue(rel.cascadeDeletes()); - assertSameRelationships(rels, db.getRelationships(t2, t1)); + assertSameRelationships(rels, db.getRelationships(t2, t1), true); rels = db.getRelationships(t2, t3); assertTrue(db.getRelationships(t2, t3).isEmpty()); - assertSameRelationships(rels, db.getRelationships(t3, t2)); + assertSameRelationships(rels, db.getRelationships(t3, t2), true); rels = db.getRelationships(t1, t3); assertEquals(1, rels.size()); @@ -84,7 +92,7 @@ public class RelationshipTest extends TestCase { assertTrue(rel.hasReferentialIntegrity()); assertEquals(256, ((RelationshipImpl)rel).getFlags()); assertTrue(rel.cascadeUpdates()); - assertSameRelationships(rels, db.getRelationships(t3, t1)); + assertSameRelationships(rels, db.getRelationships(t3, t1), true); try { db.getRelationships(t1, t1); @@ -95,10 +103,46 @@ public class RelationshipTest extends TestCase { } } - private void assertSameRelationships( - List<Relationship> expected, List<Relationship> found) + public void testOneTable() throws Exception { + for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) { + Database db = open(testDB); + Table t1 = db.getTable("Table1"); + Table t2 = db.getTable("Table2"); + Table t3 = db.getTable("Table3"); + + List<Relationship> expected = new ArrayList<Relationship>(); + expected.addAll(db.getRelationships(t1, t2)); + expected.addAll(db.getRelationships(t2, t3)); + + assertSameRelationships(expected, db.getRelationships(t2), false); + + } + } + + public void testNoTables() throws Exception { + for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) { + Database db = open(testDB); + Table t1 = db.getTable("Table1"); + Table t2 = db.getTable("Table2"); + Table t3 = db.getTable("Table3"); + + List<Relationship> expected = new ArrayList<Relationship>(); + expected.addAll(db.getRelationships(t1, t2)); + expected.addAll(db.getRelationships(t2, t3)); + expected.addAll(db.getRelationships(t1, t3)); + + assertSameRelationships(expected, db.getRelationships(), false); + } + } + + private static void assertSameRelationships( + List<Relationship> expected, List<Relationship> found, boolean ordered) { assertEquals(expected.size(), found.size()); + if(!ordered) { + Collections.sort(expected, REL_COMP); + Collections.sort(found, REL_COMP); + } for(int i = 0; i < expected.size(); ++i) { Relationship eRel = expected.get(i); Relationship fRel = found.get(i); |