]> source.dussan.org Git - jackcess.git/commitdiff
merge trunk changes through r738
authorJames Ahlborn <jtahlborn@yahoo.com>
Tue, 18 Jun 2013 02:44:20 +0000 (02:44 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Tue, 18 Jun 2013 02:44:20 +0000 (02:44 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jackcess-2@739 f203690c-595d-4dc9-a70b-905162fa7fd2

17 files changed:
pom.xml
src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/complex/Attachment.java
src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java
src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java
src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
src/java/com/healthmarketscience/jackcess/impl/PageChannel.java
src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java
src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java
src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java
src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java
src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java
src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java
test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java
test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java

diff --git a/pom.xml b/pom.xml
index 824733ffb1e6d4034434b9f8992ce2a9b7d24f4f..5d266f967df12adbebce0e65c7084118e2e01212 100644 (file)
--- a/pom.xml
+++ b/pom.xml
               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>
index 260f2667cb900474e1a425f8dd88ddcde8670820..a3e5c6f997a9c209d027d1d39768ce70e2ea2223 100644 (file)
@@ -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">
index 0044f26f2a194e379b469ce3244138a43e113687..71a6d59786bf867ff153d744eb42c49bbbd18733 100644 (file)
@@ -187,6 +187,32 @@ public interface Database extends Iterable<Table>, Closeable, Flushable
   public List<Relationship> getRelationships(Table table1, Table table2)
     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_
index 2f4b0461cf5dedfeff4c46b609e9dc8aed8dce3e..c56931572e29a3868817c757fc67adabc6dd8c0a 100644 (file)
@@ -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);
index 1b651cd99cc5f914294bb49a78875d7c6c38f160..aeff8c99514bbe81d0e1eea48d05fa14757049d5 100644 (file)
@@ -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;
 
index ad2c1fe69d3139b41140d2c18aefc33a665d7d1a..857c6319d44b261e4bb25a2ce5520b2b7d95aff4 100644 (file)
@@ -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);
     }
index 465f9ea4a87284a52ab09091a45a6cefc2eec568..154d939eb016d3fd1f4166a62325c724a6b5d56b 100644 (file)
@@ -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);
 
index dafa0c5bbb5791f85003020ae046a733a17d555c..87f7ef192021ba2bdbe0e50225a1f13690e274c1 100644 (file)
@@ -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);
index d3d5367f1fe046bdaf31da2a54198d7aa9263a6b..2ea9d275999d05591a2999a990688e652f1377ad 100644 (file)
@@ -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);
+}
 }
index c0e5646522e208a80dc684dc2eda8e81a90153d8..69c43dfb6d904c23907c86f1fea13d923ab34c81 100644 (file)
@@ -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();
+        }
+      }
+    }
+  }
+
 }
index 97734960f1ffeef8b0180707264ef2ec634f838b..83e86a2acc4ed57dc879e0e82a1e35fa02a8641a 100644 (file)
@@ -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();
index d196ae55c29d1d2e2e2ff54a8dc85089297dd673..4e0cc0c6a11840b8821b71bcdb28d864d8e82b41 100644 (file)
@@ -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);
index 16b84e359300dc1a78916c73903f379b69c74e30..5f33688a641a277bee938c7f3ee07ac51a1ffe9a 100644 (file)
@@ -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;
index 84dbcdb85bea3d062c88752b19f998d771057a59..d84f050342bd980db8e5d0f0796cfd87e48d5f5e 100644 (file)
@@ -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();
index c1ebbfd0b3428285ec07402eea5d9c2459799df4..c08d1f1ebfad7e84161b38b0fa1ad9b9e0052058 100644 (file)
@@ -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());
index a33b64ad342d52cdb02f8bde59cd31c81e48784a..173a53cf5e44b1eb3ec3d5ee63b808e1b4636574 100644 (file)
@@ -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.");
+  
 }
index c43ef9f5553de9c4bd8a4c1d25bb02bdc64a3569..e2162c344b31a95bbea824103c4fe562e4c7881f 100644 (file)
@@ -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);