]> source.dussan.org Git - jackcess.git/commitdiff
add attachment encoding support
authorJames Ahlborn <jtahlborn@yahoo.com>
Tue, 4 Jun 2013 03:10:46 +0000 (03:10 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Tue, 4 Jun 2013 03:10:46 +0000 (03:10 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@734 f203690c-595d-4dc9-a70b-905162fa7fd2

src/java/com/healthmarketscience/jackcess/ByteUtil.java
src/java/com/healthmarketscience/jackcess/complex/Attachment.java
src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java
src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java
src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java
src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java
src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java
src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java
test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java

index b500268e225c266db29dd92757adc885ae40fdb0..b46a44b199e50a1b6947289845250a6d5aec6b88 100644 (file)
@@ -29,6 +29,7 @@ package com.healthmarketscience.jackcess;
 
 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);
     }
index be7e3026988d03bfe598e7085295c9dafae38c5a..18c361e615736d3b45c2b52a14116bb321cad704 100644 (file)
@@ -29,7 +29,7 @@ import java.util.Date;
  */
 public interface Attachment extends ComplexValue 
 {
-  public byte[] getFileData();
+  public byte[] getFileData() throws IOException;
 
   public void setFileData(byte[] data);
 
index eb3ca7387d7260e2d710a426e18c483836f5eacc..1d0595cf8235f4dc4574b43191fd7095785030e4 100644 (file)
@@ -23,14 +23,21 @@ 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.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;
 
@@ -42,12 +49,21 @@ 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;
@@ -155,7 +171,9 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
   }
 
   @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());
@@ -286,7 +304,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
       _decodedData = decodedData;
     }
     
-    public byte[] getFileData() {
+    public byte[] getFileData() throws IOException {
       if((_data == null) && (_decodedData != null)) {
         _data = encodeData();
       }
@@ -359,12 +377,19 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<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;
     } 
 
     /**
@@ -372,7 +397,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
      */
     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");
       }
@@ -385,7 +410,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
       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
@@ -399,9 +424,9 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
 
         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();
@@ -428,9 +453,65 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
     /**
      * 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();
+        }
+      }
     }
   }
 
index 0a4b25535d2fdc5ad9fc4dd5d82a375535c81a56..3dac47c6aaace9ae89f4edb3464f7a8c9467c65e 100644 (file)
@@ -349,7 +349,9 @@ public abstract class ComplexColumnInfo<V extends ComplexValue>
     _pkCursor.deleteCurrentRow();
   }
   
-  protected Object[] asRow(Object[] row, V value) {
+  protected Object[] asRow(Object[] row, V value)
+    throws IOException
+  {
     int id = value.getId();
     _pkCol.setRowValue(row, ((id != INVALID_ID) ? id : Column.AUTO_NUMBER));
     int cId = value.getComplexValueForeignKey().get();
index 170a92ff3db05907bd8cb1526b7f9c88c5b66fef..13e6b7a65b41c7cf0e3c4d6409040c6d2ebfc250 100644 (file)
@@ -327,9 +327,7 @@ public class ComplexValueForeignKey extends Number
   }
 
   @Override
-  public String toString()
-  {
+  public String toString() {
     return String.valueOf(_value);
-  }
-  
+  }  
 }
index b1bec203164cdec06e282bcee00aa5c4af6811cc..efbd8b0ab6640ebb3b82409b48d730de5fec8a92 100644 (file)
@@ -73,7 +73,7 @@ public class MultiValueColumnInfo extends ComplexColumnInfo<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;
index 03bd8b1d6ff68d19d24741840344828a9ccb2f40..0eda7f739e98fa0058838b12c7ba7ff662335362 100644 (file)
@@ -68,7 +68,9 @@ public class UnsupportedColumnInfo extends ComplexColumnInfo<UnsupportedValue>
   }
 
   @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();
index 8fc56221ba4d4988fe4ea14d360ed883668b0008..c8df424496163035681c65fc042456f65fa994ed 100644 (file)
@@ -135,7 +135,7 @@ public class VersionHistoryColumnInfo extends ComplexColumnInfo<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());
index 6dbcbed524572053a07d8acfd8310a8839aa6a14..2122b5309512835733eb996088d48bc8c8bc1835 100644 (file)
@@ -189,6 +189,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.addDecodedAttachment(null, "test_data2.txt", "txt",
+                                     getDecodedFileBytes("test_data2.txt"), null,
+                                     null);
+      checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt", 
+                       "test_data2.txt");
 
       Cursor cursor = Cursor.createCursor(t1);
       assertTrue(cursor.findFirstRow(t1.getColumn("id"), "row4"));