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>
<action dev="jahlborn" type="update">
Add more methods to Database for retrieving Relationships.
</action>
+ <action dev="jahlborn" type="update">
+ Add methods to get the actual attachment content, thanks to Lorenzo
+ Carrara.
+ </action>
</release>
<release version="1.2.12" date="2013-05-09">
<action dev="jahlborn" type="fix" system="SourceForge2" issue="94">
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);
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;
}
.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);
+ }
}
package com.healthmarketscience.jackcess.complex;
+import java.io.IOException;
import java.util.Date;
/**
public void setFileData(byte[] data);
+ public byte[] getDecodedFileData() throws IOException;
+
+ public void setDecodedFileData(byte[] data);
+
public String getFileName();
public void setFileName(String fileName);
package com.healthmarketscience.jackcess.complex;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
import java.util.Date;
import java.util.List;
import java.util.Map;
+import java.util.zip.InflaterInputStream;
import com.healthmarketscience.jackcess.ByteUtil;
import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.PageChannel;
import com.healthmarketscience.jackcess.Table;
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 final Column _fileUrlCol;
private final Column _fileNameCol;
private final Column _fileTypeCol;
byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue);
return new AttachmentImpl(id, complexValueFk, url, name, type, data,
- ts, flags);
+ ts, flags, null);
}
@Override
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 newDecodedAttachment(byte[] decodedData) {
+ return newDecodedAttachment(INVALID_COMPLEX_VALUE_ID, decodedData);
+ }
+
+ public static Attachment newDecodedAttachment(
+ ComplexValueForeignKey complexValueFk, byte[] decodedData) {
+ return newDecodedAttachment(complexValueFk, null, null, null, decodedData,
+ null, null);
+ }
+
+ public static Attachment newDecodedAttachment(
+ String url, String name, String type, byte[] decodedData,
+ Date timeStamp, Integer flags)
+ {
+ return newDecodedAttachment(INVALID_COMPLEX_VALUE_ID, url, name, type,
+ decodedData, timeStamp, flags);
+ }
+
+ public static Attachment newDecodedAttachment(
+ ComplexValueForeignKey complexValueFk, String url, String name,
+ String type, byte[] decodedData, Date timeStamp, Integer flags)
+ {
+ return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type,
+ null, timeStamp, flags, decodedData);
}
private byte[] _data;
private Date _timeStamp;
private Integer _flags;
+ private byte[] _decodedData;
private AttachmentImpl(int id, ComplexValueForeignKey complexValueFk,
String url, String name, String type, byte[] data,
- Date timeStamp, Integer flags)
+ Date timeStamp, Integer flags, byte[] decodedData)
{
super(id, complexValueFk);
_url = url;
_data = data;
_timeStamp = timeStamp;
_flags = flags;
+ _decodedData = decodedData;
}
public byte[] getFileData() {
+ if((_data == null) && (_decodedData != null)) {
+ _data = encodeData();
+ }
return _data;
}
public void setFileData(byte[] data) {
_data = data;
+ _decodedData = null;
+ }
+
+ public byte[] getDecodedFileData() throws IOException {
+ if((_decodedData == null) && (_data != null)) {
+ _decodedData = decodeData();
+ }
+ return _decodedData;
+ }
+
+ public void setDecodedFileData(byte[] data) {
+ _decodedData = data;
+ _data = null;
}
public String getFileName() {
+ ", " + getFileTimeStamp() + ", " + getFileFlags() + ", " +
ByteUtil.toHexString(getFileData());
}
+
+ /**
+ * Decodes the raw attachment file data to get the _actual_ content.
+ */
+ private byte[] decodeData() throws IOException {
+
+ if(_data.length < 8) {
+ // nothing we can do
+ throw new IOException("Unknown encoded attachment data format");
+ }
+
+ // read initial header info
+ ByteBuffer bb = PageChannel.wrap(_data);
+ int typeFlag = bb.getInt();
+ int dataLen = bb.getInt();
+
+ DataInputStream contentStream = null;
+ try {
+ InputStream bin = new ByteArrayInputStream(
+ _data, 8, _data.length - 8);
+
+ 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 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 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() {
+ // FIXME, writeme
+ throw new UnsupportedOperationException();
+ }
}
-
+
}
return a;
}
+ public Attachment addDecodedAttachment(byte[] decodedData)
+ throws IOException
+ {
+ return addDecodedAttachment(null, null, null, decodedData, null, null);
+ }
+
+ public Attachment addDecodedAttachment(
+ String url, String name, String type, byte[] decodedData,
+ Date timeStamp, Integer flags)
+ throws IOException
+ {
+ reset();
+ Attachment a = AttachmentColumnInfo.newDecodedAttachment(
+ this, url, name, type, decodedData, timeStamp, flags);
+ getAttachmentInfo().addValue(a);
+ return a;
+ }
+
public Attachment updateAttachment(Attachment attachment)
throws IOException
{
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(getDecodedFileBytes(fname),
+ a.getDecodedFileData()));
}
}
}
throw new RuntimeException("unexpected bytes");
}
+ private static byte[] getDecodedFileBytes(String fname) throws Exception
+ {
+ if("test_data.txt".equals(fname)) {
+ return TEST_DEC_BYTES;
+ }
+ if("test_data2.txt".equals(fname)) {
+ return TEST2_DEC_BYTES;
+ }
+ throw new RuntimeException("unexpected bytes");
+ }
+
private static byte b(int i) { return (byte)i; }
+ private static byte[] getAsciiBytes(String str) {
+ try {
+ return str.getBytes("US-ASCII");
+ } catch(Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
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[] TEST_DEC_BYTES = getAsciiBytes("this is some test data for attachment.");
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)
};
+
+ private static final byte[] TEST2_DEC_BYTES = getAsciiBytes("this is some more test data for attachment.");
+
}