import java.io.FileWriter;
import java.io.IOException;
+import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
* 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;
}
}
+ @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);
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);
}
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.List;
import java.util.Map;
+import java.util.Set;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import com.healthmarketscience.jackcess.ByteUtil;
import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.JetFormat;
import com.healthmarketscience.jackcess.PageChannel;
import com.healthmarketscience.jackcess.Table;
*/
public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
{
+ /** 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;
}
@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());
_decodedData = decodedData;
}
- public byte[] getFileData() {
+ public byte[] getFileData() throws IOException {
if((_data == null) && (_decodedData != null)) {
_data = encodeData();
}
}
@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;
}
/**
*/
private byte[] decodeData() throws IOException {
- if(_data.length < 8) {
+ if(_data.length < WRAPPER_HEADER_SIZE) {
// nothing we can do
throw new IOException("Unknown encoded attachment data format");
}
DataInputStream contentStream = null;
try {
InputStream bin = new ByteArrayInputStream(
- _data, 8, _data.length - 8);
+ _data, WRAPPER_HEADER_SIZE, _data.length - WRAPPER_HEADER_SIZE);
if(typeFlag == DATA_TYPE_RAW) {
// nothing else to do
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
+ // 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();
/**
* Encodes the actual attachment file data to get the raw, stored format.
*/
- private byte[] encodeData() {
- // FIXME, writeme
- throw new UnsupportedOperationException();
+ 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 = Column.encodeUncompressedText(
+ type, JetFormat.VERSION_12.CHARSET);
+ int headerLen = typeBytes.remaining() + CONTENT_HEADER_SIZE;
+
+ int dataLen = _decodedData.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(_decodedData);
+ contentStream.close();
+ contentStream = null;
+
+ return dataStream.toByteArray();
+
+ } finally {
+ if(contentStream != null) {
+ try {
+ contentStream.close();
+ } catch(IOException e) {
+ // ignored
+ }
+ }
+ if(deflater != null) {
+ deflater.end();
+ }
+ }
}
}