]> source.dussan.org Git - jackcess.git/commitdiff
initial support for blob creation/parsing
authorJames Ahlborn <jtahlborn@yahoo.com>
Thu, 12 Sep 2013 12:20:16 +0000 (12:20 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Thu, 12 Sep 2013 12:20:16 +0000 (12:20 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@797 f203690c-595d-4dc9-a70b-905162fa7fd2

pom.xml
src/changes/changes.xml
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java
src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java
src/main/java/com/healthmarketscience/jackcess/util/OleBlob.java [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index 155c147e8d8e405f7d43215e9206577b42edf150..ce22f5b07fd8b792702b51467d929d0811632fe6 100644 (file)
--- a/pom.xml
+++ b/pom.xml
       <artifactId>junit</artifactId>
       <version>4.11</version>
       <scope>test</scope>
-    </dependency>    
+    </dependency>
+
+    <!-- Only necessary if working with compound ole data -->
+    <dependency>
+      <groupId>org.apache.poi</groupId>
+      <artifactId>poi</artifactId>
+      <version>3.9</version>
+      <optional>true</optional>
+    </dependency>
+    
   </dependencies>
   <reporting>
     <plugins>
index 339fde8769695227380a983c598ece0171a44632..a4a33f486b0a19b4912bd33c5216d5e7902f709a 100644 (file)
@@ -4,6 +4,11 @@
     <author email="javajedi@users.sf.net">Tim McCune</author>
   </properties>
   <body>
+    <release version="2.0.1" date="TBD">
+      <action dev="jahlborn" type="add">
+        Add initial support for creating/parsing ole content.
+      </action>
+    </release>
     <release version="2.0.0" date="2013-08-26">
       <action dev="jahlborn" type="update">
         Brand new API!  This release is not backwards compatible with 1.x
index 1a71d95329140b8e3238683ea501243afc6ca859..0f9714de7a041e901c27a79cbda0bd1e87c11037 100644 (file)
@@ -1709,6 +1709,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
       return null;
     } else if(value instanceof byte[]) {
       return (byte[])value;
+    } else if(value instanceof OleUtil.OleBlobImpl) {
+      return ((OleUtil.OleBlobImpl)value).getBytes();
     } else if(value instanceof Blob) {
       try {
         Blob b = (Blob)value;
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java
new file mode 100644 (file)
index 0000000..bcf3255
--- /dev/null
@@ -0,0 +1,202 @@
+/*
+Copyright (c) 2013 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.impl;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.healthmarketscience.jackcess.impl.OleUtil.*;
+import com.healthmarketscience.jackcess.util.MemFileChannel;
+import static com.healthmarketscience.jackcess.util.OleBlob.*;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.poi.poifs.filesystem.DirectoryEntry;
+import org.apache.poi.poifs.filesystem.DocumentEntry;
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.poifs.filesystem.Entry;
+import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
+
+/**
+ * Utility code for working with OLE data which is in the compound storage
+ * format.  This functionality relies on the optional POI library.
+ * <p/> 
+ * Note that all POI usage is restricted to this file so that the basic ole
+ * support in OleUtil can be utilized without requiring POI.
+ *
+ * @author James Ahlborn
+ */
+public class CompoundOleUtil implements OleUtil.CompoundPackageFactory
+{
+  private static final String ENTRY_NAME_CHARSET = "UTF-8";
+  private static final String ENTRY_SEPARATOR = "/";
+  private static final String CONTENTS_ENTRY = "CONTENTS";
+
+  public CompoundOleUtil() 
+  {
+  }
+
+  public ContentImpl createCompoundPackageContent(
+      OleBlobImpl blob, String prettyName, String className, String typeName,
+      ByteBuffer blobBb, int dataBlockLen)
+  {
+    return new CompoundContentImpl(blob, prettyName, className, typeName,
+                                   blobBb.position(), dataBlockLen);
+  }
+
+  private static String encodeEntryName(String name) {
+    try {
+      return URLEncoder.encode(name, ENTRY_NAME_CHARSET);
+    } catch(UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static String decodeEntryName(String name) {
+    try {
+      return URLDecoder.decode(name, ENTRY_NAME_CHARSET);
+    } catch(UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static final class CompoundContentImpl 
+    extends EmbeddedPackageContentImpl
+    implements CompoundContent
+  {
+    private NPOIFSFileSystem _fs;
+
+    private CompoundContentImpl(
+        OleBlobImpl blob, String prettyName, String className,
+        String typeName, int position, int length) 
+    {
+      super(blob, prettyName, className, typeName, position, length);
+    }        
+
+    public ContentType getType() {
+      return ContentType.COMPOUND_STORAGE;
+    }
+
+    private NPOIFSFileSystem getFileSystem() throws IOException {
+      if(_fs == null) {
+        _fs = new NPOIFSFileSystem(MemFileChannel.newChannel(getStream(), "r"));
+      }
+      return _fs;
+    }
+
+    public List<String> getEntries() throws IOException {
+      return getEntries(new ArrayList<String>(), getFileSystem().getRoot(),
+                        ENTRY_SEPARATOR, false);
+    }
+
+    public InputStream getEntryStream(String entryName) throws IOException {
+      return new DocumentInputStream(getDocumentEntry(entryName));
+    }
+
+    public boolean hasContentsEntry() throws IOException {
+      return getFileSystem().getRoot().hasEntry(CONTENTS_ENTRY);
+    }
+
+    public InputStream getContentsEntryStream() throws IOException {
+      return getEntryStream(CONTENTS_ENTRY);
+    }
+
+    private DocumentEntry getDocumentEntry(String entryName) throws IOException {
+
+      // split entry name into individual components and decode them
+      List<String> entryNames = new ArrayList<String>();
+      for(String str : entryName.split(ENTRY_SEPARATOR)) {
+        if(str.length() == 0) {
+          continue;
+        }
+        entryNames.add(decodeEntryName(str));
+      }
+
+      DirectoryEntry dir = getFileSystem().getRoot();
+      DocumentEntry entry = null;
+      Iterator<String> iter = entryNames.iterator();
+      while(iter.hasNext()) {
+        Entry tmpEntry = dir.getEntry(iter.next());
+        if(tmpEntry instanceof DirectoryEntry) {
+          dir = (DirectoryEntry)tmpEntry;
+        } else if(!iter.hasNext() && (tmpEntry instanceof DocumentEntry)) {
+          entry = (DocumentEntry)tmpEntry;
+        } else {
+          break;
+        }        
+      }
+      
+      if(entry == null) {
+        throw new FileNotFoundException("Could not find document " + entryName);
+      }
+
+      return entry;
+    }
+
+    private List<String> getEntries(List<String> entries, DirectoryEntry dir, 
+                                    String prefix, boolean includeDetails) {
+      for(Entry entry : dir) {
+        if (entry instanceof DirectoryEntry) {
+          // .. recurse into this directory
+          getEntries(entries, (DirectoryEntry)entry, prefix + ENTRY_SEPARATOR,
+                     includeDetails);
+        } else if(entry instanceof DocumentEntry) {
+          // grab the entry name/detils
+          String entryName = prefix + encodeEntryName(entry.getName());
+          if(includeDetails) {
+            entryName += " (" + ((DocumentEntry)entry).getSize() + ")";
+          }
+          entries.add(entryName);
+        }
+      }
+      return entries;
+    }
+
+    @Override
+    public void close() {
+      ByteUtil.closeQuietly(_fs);
+      _fs = null;
+      super.close();
+    }
+
+    @Override
+    public String toString() {
+      ToStringBuilder sb = toString(CustomToStringStyle.builder(this));
+
+      try {
+        sb.append("hasContentsEntry", hasContentsEntry());
+        sb.append("entries",
+                  getEntries(new ArrayList<String>(), getFileSystem().getRoot(),
+                             ENTRY_SEPARATOR, true));
+      } catch(IOException e) {  
+        sb.append("entries", "<" + e + ">");
+      }
+
+      return sb.toString();
+    }
+  }
+
+}
index f6b4ade55fd10497eac3f9c80b206c4ea9ce92d7..8247f4f7c6093bf4229d2acf0d2f0f1323d1b0be 100644 (file)
@@ -1125,7 +1125,7 @@ public class IndexPageCache
         }
       }
     } catch(IOException e) {
-      pages.add("DataPage[" + dpMain._pageNumber + "]: " + e);
+      pages.add("DataPage[" + dpMain._pageNumber + "]: <" + e + ">");
     }
     return pages;
   }
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java
new file mode 100644 (file)
index 0000000..b82ded2
--- /dev/null
@@ -0,0 +1,778 @@
+/*
+Copyright (c) 2013 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.sql.Blob;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.EnumSet;
+import java.util.Set;
+
+import com.healthmarketscience.jackcess.DataType;
+import com.healthmarketscience.jackcess.util.OleBlob;
+import static com.healthmarketscience.jackcess.util.OleBlob.*;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+/**
+ * Utility code for working with OLE data.
+ *
+ * @author James Ahlborn
+ */
+public class OleUtil 
+{
+  /**
+   * Interface used to allow optional inclusion of the poi library for working
+   * with compound ole data.
+   */
+  public interface CompoundPackageFactory
+  {
+    public ContentImpl createCompoundPackageContent(
+        OleBlobImpl blob, String prettyName, String className, String typeName,
+        ByteBuffer blobBb, int dataBlockLen);
+  }
+
+  private static final int PACKAGE_SIGNATURE = 0x1C15;
+  private static final Charset OLE_CHARSET = Charset.forName("US-ASCII");
+  private static final Charset OLE_UTF_CHARSET = Charset.forName("UTF-16LE");
+  private static final byte[] COMPOUND_STORAGE_SIGNATURE = 
+    {(byte)0xd0,(byte)0xcf,(byte)0x11,(byte)0xe0,
+     (byte)0xa1,(byte)0xb1,(byte)0x1a,(byte)0xe1};
+  private static final String SIMPLE_PACKAGE_TYPE = "Package";
+  private static final int PACKAGE_OBJECT_TYPE = 0x02;
+  private static final int OLE_VERSION = 0x0501;
+  private static final int OLE_FORMAT = 0x02;
+  private static final int PACKAGE_STREAM_SIGNATURE = 0x02;
+  private static final int PS_EMBEDDED_FILE = 0x030000;
+  private static final int PS_LINKED_FILE = 0x010000;
+  private static final Set<ContentType> WRITEABLE_TYPES = EnumSet.of(
+      ContentType.LINK, ContentType.SIMPLE_PACKAGE, ContentType.OTHER);
+  private static final byte[] NO_DATA = new byte[0];
+  private static final int LINK_HEADER = 0x01;
+  private static final byte[] PACKAGE_FOOTER = {
+    0x01, 0x05, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x01, (byte)0xAD, 0x05, (byte)0xFE
+  };
+
+  private static final CompoundPackageFactory COMPOUND_FACTORY;
+
+  static {
+    CompoundPackageFactory compoundFactory = null;
+    try {
+      compoundFactory = (CompoundPackageFactory)
+        Class.forName("com.healthmarketscience.jackcess.impl.CompoundOleUtil")
+        .newInstance();
+    } catch(Exception e) {
+      // must not have poi, will load compound ole data as "other"
+    }
+    COMPOUND_FACTORY = compoundFactory;
+  }
+
+  public static OleBlob parseBlob(byte[] bytes) {
+    return new OleBlobImpl(bytes);
+  }
+
+  public static OleBlob createBlob(OleBlob.Builder oleBuilder)
+    throws IOException
+  {
+    try {
+      
+      if(!WRITEABLE_TYPES.contains(oleBuilder.getType())) {
+        throw new IllegalArgumentException(
+            "Cannot currently create ole values of type " +
+            oleBuilder.getType());
+      }
+      
+      long contentLen = oleBuilder.getContentLength();
+      byte[] contentBytes = oleBuilder.getBytes();
+      InputStream contentStream = oleBuilder.getStream();
+      byte[] packageStreamHeader = NO_DATA;
+      byte[] packageStreamFooter = NO_DATA;
+
+      switch(oleBuilder.getType()) {
+      case LINK:
+        packageStreamHeader = writePackageStreamHeader(oleBuilder);
+
+        // link "content" is file path
+        contentBytes = getZeroTermStrBytes(oleBuilder.getFilePath());
+        contentLen = contentBytes.length;
+        break;
+        
+      case SIMPLE_PACKAGE:
+        packageStreamHeader = writePackageStreamHeader(oleBuilder);
+        packageStreamFooter = writePackageStreamFooter(oleBuilder);
+        break;
+        
+      case OTHER:
+        // nothing more to do
+        break;
+      default:
+        throw new RuntimeException("unexpected type " + oleBuilder.getType());
+      }
+
+      long payloadLen = packageStreamHeader.length + packageStreamFooter.length +
+        contentLen;
+      byte[] packageHeader = writePackageHeader(oleBuilder, payloadLen);
+            
+      long totalOleLen = packageHeader.length + PACKAGE_FOOTER.length +
+        payloadLen;
+      if(totalOleLen > DataType.OLE.getMaxSize()) {
+        throw new IllegalArgumentException("Content size of " + totalOleLen +
+                                           " is too large for ole column");
+      }
+      
+      byte[] oleBytes = new byte[(int)totalOleLen];
+      ByteBuffer bb = PageChannel.wrap(oleBytes);
+      bb.put(packageHeader);
+      bb.put(packageStreamHeader);
+      
+      if(contentLen > 0L) {
+        if(contentBytes != null) {
+          bb.put(contentBytes);
+        } else {
+          byte[] buf = new byte[8192];
+          int numBytes = 0;
+          while((numBytes = contentStream.read(buf)) >= 0) {
+            bb.put(buf, 0, numBytes);
+          }
+        }
+      }
+
+      bb.put(packageStreamFooter);
+      bb.put(PACKAGE_FOOTER);
+    
+      return parseBlob(oleBytes);
+      
+    } finally {
+      ByteUtil.closeQuietly(oleBuilder.getStream());
+    }
+  }
+
+  private static byte[] writePackageHeader(OleBlob.Builder oleBuilder,
+                                           long contentLen) {
+
+    byte[] prettyNameBytes = getZeroTermStrBytes(oleBuilder.getPrettyName());
+    String className = oleBuilder.getClassName();
+    String typeName = oleBuilder.getTypeName();
+    if(className == null) {
+      className = typeName;
+    } else if(typeName == null) {
+      typeName = className;
+    }
+    byte[] classNameBytes = getZeroTermStrBytes(className);
+    byte[] typeNameBytes = getZeroTermStrBytes(typeName);
+    
+    int packageHeaderLen = 20 + prettyNameBytes.length + classNameBytes.length;
+
+    int oleHeaderLen = 24 + typeNameBytes.length;
+
+    byte[] headerBytes = new byte[packageHeaderLen + oleHeaderLen];
+    
+    ByteBuffer bb = PageChannel.wrap(headerBytes);
+
+    // write outer package header
+    bb.putShort((short)PACKAGE_SIGNATURE);
+    bb.putShort((short)packageHeaderLen);
+    bb.putInt(PACKAGE_OBJECT_TYPE);
+    bb.putShort((short)prettyNameBytes.length);
+    bb.putShort((short)classNameBytes.length);
+    int prettyNameOff = bb.position() + 8;
+    bb.putShort((short)prettyNameOff);
+    bb.putShort((short)(prettyNameOff + prettyNameBytes.length));
+    bb.putInt(-1);
+    bb.put(prettyNameBytes);
+    bb.put(classNameBytes);
+
+    // put ole header
+    bb.putInt(OLE_VERSION);
+    bb.putInt(OLE_FORMAT);
+    bb.putInt(typeNameBytes.length);
+    bb.put(typeNameBytes);
+    bb.putLong(0L);
+    bb.putInt((int)contentLen);
+    
+    return headerBytes;
+  }
+
+  private static byte[] writePackageStreamHeader(OleBlob.Builder oleBuilder) {
+
+    byte[] fileNameBytes = getZeroTermStrBytes(oleBuilder.getFileName());
+    byte[] filePathBytes = getZeroTermStrBytes(oleBuilder.getFilePath());
+
+    int headerLen = 6 + fileNameBytes.length + filePathBytes.length;
+
+    if(oleBuilder.getType() == ContentType.SIMPLE_PACKAGE) {
+
+      headerLen += 8 + filePathBytes.length;
+      
+    } else {
+
+      headerLen += 2;
+    }
+
+    byte[] headerBytes = new byte[headerLen];
+    ByteBuffer bb = PageChannel.wrap(headerBytes);
+    bb.putShort((short)PACKAGE_STREAM_SIGNATURE);
+    bb.put(fileNameBytes);
+    bb.put(filePathBytes);
+
+    if(oleBuilder.getType() == ContentType.SIMPLE_PACKAGE) {
+      bb.putInt(PS_EMBEDDED_FILE);
+      bb.putInt(filePathBytes.length);
+      bb.put(filePathBytes, 0, filePathBytes.length);
+      bb.putInt((int) oleBuilder.getContentLength());
+    } else {
+      bb.putInt(PS_LINKED_FILE);
+      bb.putShort((short)LINK_HEADER);
+    }
+    
+    return headerBytes;
+  }
+  
+  private static byte[] writePackageStreamFooter(OleBlob.Builder oleBuilder) {
+
+    // note, these are _not_ zero terminated
+    byte[] fileNameBytes = oleBuilder.getFileName().getBytes(OLE_UTF_CHARSET);
+    byte[] filePathBytes = oleBuilder.getFilePath().getBytes(OLE_UTF_CHARSET);
+
+    int footerLen = 12 + (filePathBytes.length * 2) + fileNameBytes.length;
+
+    byte[] footerBytes = new byte[footerLen];
+    ByteBuffer bb = PageChannel.wrap(footerBytes);
+
+    bb.putInt(filePathBytes.length/2);
+    bb.put(filePathBytes);
+    bb.putInt(fileNameBytes.length/2);
+    bb.put(fileNameBytes);
+    bb.putInt(filePathBytes.length/2);
+    bb.put(filePathBytes);    
+
+    return footerBytes;
+  }
+  
+  private static ContentImpl createContent(OleBlobImpl blob) 
+    throws IOException 
+  {
+    ByteBuffer bb = PageChannel.wrap(blob.getBytes());
+
+    if((bb.remaining() < 2) || (bb.getShort() != PACKAGE_SIGNATURE)) {  
+      return new UnknownContentImpl(blob);
+    }
+
+    // read outer package header
+    int headerSize = bb.getShort();
+    int objType = bb.getInt();
+    int prettyNameLen = bb.getShort();
+    int classNameLen = bb.getShort();
+    int prettyNameOff = bb.getShort();
+    int classNameOff = bb.getShort();       
+    int objSize = bb.getInt();
+    String prettyName = readStr(bb, prettyNameOff, prettyNameLen);
+    String className = readStr(bb, classNameOff, classNameLen);
+    bb.position(headerSize);
+
+    // read ole header
+    int oleVer = bb.getInt();
+    int format = bb.getInt();
+
+    if(oleVer != OLE_VERSION) {
+      return new UnknownContentImpl(blob);
+    }
+
+    int typeNameLen = bb.getInt();
+    String typeName = readStr(bb, bb.position(), typeNameLen);
+    bb.getLong(); // unused
+    int dataBlockLen = bb.getInt();
+    int dataBlockPos = bb.position();
+
+
+    if(SIMPLE_PACKAGE_TYPE.equalsIgnoreCase(typeName)) {
+      return createSimplePackageContent(
+          blob, prettyName, className, typeName, bb, dataBlockLen);
+    }
+
+    // if COMPOUND_FACTORY is null, the poi library isn't available, so just
+    // load compound data as "other"
+    if((COMPOUND_FACTORY != null) &&
+       (bb.remaining() >= COMPOUND_STORAGE_SIGNATURE.length) &&
+       ByteUtil.matchesRange(bb, bb.position(), COMPOUND_STORAGE_SIGNATURE)) {
+      return COMPOUND_FACTORY.createCompoundPackageContent(
+          blob, prettyName, className, typeName, bb, dataBlockLen);
+    }
+    
+    // this is either some other "special" (as yet unhandled) format, or it is
+    // simply an embedded file (or it is compound data and poi isn't available)
+    return new OtherContentImpl(blob, prettyName, className,
+                                typeName, dataBlockPos, dataBlockLen);
+  }
+
+  private static ContentImpl createSimplePackageContent(
+      OleBlobImpl blob, String prettyName, String className, String typeName,
+      ByteBuffer blobBb, int dataBlockLen) {
+
+    int dataBlockPos = blobBb.position();
+    ByteBuffer bb = PageChannel.narrowBuffer(blobBb, dataBlockPos, 
+                                             dataBlockPos + dataBlockLen);
+    
+    int packageSig = bb.getShort();
+    if(packageSig != PACKAGE_STREAM_SIGNATURE) {
+      return new OtherContentImpl(blob, prettyName, className,
+                                  typeName, dataBlockPos, dataBlockLen);
+    }
+
+    String fileName = readZeroTermStr(bb);
+    String filePath = readZeroTermStr(bb);
+    int packageType = bb.getInt();
+
+    if(packageType == PS_EMBEDDED_FILE) {
+
+      int localFilePathLen = bb.getInt();
+      String localFilePath = readStr(bb, bb.position(), localFilePathLen);
+      int dataLen = bb.getInt();
+      int dataPos = bb.position();
+      bb.position(dataLen + dataPos);
+
+      // remaining strings are in "reverse" order (local file path, file name,
+      // file path).  these string usee a real utf charset, and therefore can
+      // "fix" problems with ascii based names (so we prefer these strings to
+      // the original strings we found)
+      int strNum = 0;
+      while(true) {
+
+        int rem = bb.remaining();
+        if(rem < 4) {
+          break;
+        }
+
+        int strLen = bb.getInt();
+        String remStr = readStr(bb, bb.position(), strLen * 2, OLE_UTF_CHARSET);
+
+        switch(strNum) {
+        case 0:
+          localFilePath = remStr;
+          break;
+        case 1:
+          fileName = remStr;
+          break;
+        case 2:
+          filePath = remStr;
+          break;
+        default:
+          // ignore
+        }
+
+        ++strNum;
+      }
+
+      return new SimplePackageContentImpl(
+          blob, prettyName, className, typeName, dataPos, dataLen,
+          fileName, filePath, localFilePath);
+    } 
+
+    if(packageType == PS_LINKED_FILE) {
+      
+      bb.getShort(); //unknown
+      String linkStr = readZeroTermStr(bb);
+
+      return new LinkContentImpl(blob, prettyName, className, typeName, 
+                                 fileName, linkStr, filePath);
+    }
+
+    return new OtherContentImpl(blob, prettyName, className,
+                                typeName, dataBlockPos, dataBlockLen);      
+  }
+
+  private static String readStr(ByteBuffer bb, int off, int len) {
+    return readStr(bb, off, len, OLE_CHARSET);
+  }
+
+  private static String readZeroTermStr(ByteBuffer bb) {
+    int off = bb.position();
+    while(bb.hasRemaining()) {
+      byte b = bb.get();
+      if(b == 0) {
+        break;
+      }
+    }
+    int len = bb.position() - off;
+    return readStr(bb, off, len);
+  }
+
+  private static String readStr(ByteBuffer bb, int off, int len, 
+                                Charset charset) {
+    String str = new String(bb.array(), off, len, charset);
+    bb.position(off + len);
+    if(str.charAt(str.length() - 1) == '\0') {
+      str = str.substring(0, str.length() - 1);
+    }
+    return str;
+  }
+
+  private static byte[] getZeroTermStrBytes(String str) {
+    return (str + '\0').getBytes(OLE_CHARSET);
+  }
+
+  static final class OleBlobImpl implements OleBlob
+  {
+    private byte[] _bytes;
+    private ContentImpl _content;
+
+    private OleBlobImpl(byte[] bytes) {
+      _bytes = bytes;
+    }
+
+    public void writeTo(OutputStream out) throws IOException {
+      out.write(_bytes);
+    }
+
+    public Content getContent() throws IOException {
+      if(_content == null) {
+        _content = createContent(this);
+      }
+      return _content;
+    }
+
+    public InputStream getBinaryStream() throws SQLException {
+      return new ByteArrayInputStream(_bytes);
+    }
+
+    public InputStream getBinaryStream(long pos, long len) 
+      throws SQLException 
+    {
+      return new ByteArrayInputStream(_bytes, fromJdbcOffset(pos), (int)len);
+    }
+
+    public long length() throws SQLException {
+      return _bytes.length;
+    }
+
+    public byte[] getBytes() throws IOException {
+      if(_bytes == null) {
+        throw new IOException("blob is closed");
+      }
+      return _bytes;
+    }
+
+    public byte[] getBytes(long pos, int len) throws SQLException {
+      return ByteUtil.copyOf(_bytes, fromJdbcOffset(pos), len);
+    }
+
+    public long position(byte[] pattern, long start) throws SQLException {
+      int pos = ByteUtil.findRange(PageChannel.wrap(_bytes), 
+                                   fromJdbcOffset(start), pattern);
+      return((pos >= 0) ? toJdbcOffset(pos) : pos);
+    }
+    
+    public long position(Blob pattern, long start) throws SQLException {
+      return position(pattern.getBytes(1L, (int)pattern.length()), start);
+    }
+
+    public OutputStream setBinaryStream(long position) throws SQLException {
+      throw new SQLFeatureNotSupportedException();
+    }
+    
+    public void truncate(long len) throws SQLException {
+      throw new SQLFeatureNotSupportedException();
+    }
+    
+    public int setBytes(long pos, byte[] bytes) throws SQLException {
+      throw new SQLFeatureNotSupportedException();
+    }
+    
+    public int setBytes(long pos, byte[] bytes, int offset, int lesn)
+      throws SQLException {
+      throw new SQLFeatureNotSupportedException();
+    }
+    
+    public void free() {
+      close();
+    }
+
+    public void close() {
+      _bytes = null;
+      ByteUtil.closeQuietly(_content);
+      _content = null;
+    }
+
+    private static int toJdbcOffset(int off) {
+      return off + 1;
+    } 
+
+    private static int fromJdbcOffset(long off) {
+      return (int)off - 1;
+    } 
+
+    @Override
+    public String toString() {
+      ToStringBuilder sb = CustomToStringStyle.builder(this);
+      if(_content != null) {
+        sb.append("content", _content);
+      } else {
+        sb.append("bytes", _bytes);
+        sb.append("content", "(uninitialized)");
+      }
+      return sb.toString();
+    }
+  }
+
+  static abstract class ContentImpl implements Content, Closeable
+  {
+    protected final OleBlobImpl _blob;
+
+    protected ContentImpl(OleBlobImpl blob) {
+      _blob = blob;
+    }
+
+    public OleBlobImpl getBlob() {
+      return _blob;
+    }
+
+    protected byte[] getBytes() throws IOException {
+      return getBlob().getBytes();
+    }
+    
+    public void close() {
+      // base does nothing
+    }
+
+    protected ToStringBuilder toString(ToStringBuilder sb) {
+      sb.append("type", getType());
+      return sb;
+    } 
+  }
+
+  static abstract class EmbeddedContentImpl extends ContentImpl
+    implements EmbeddedContent
+  {
+    private final int _position;
+    private final int _length;
+
+    protected EmbeddedContentImpl(OleBlobImpl blob, int position, int length) 
+    {
+      super(blob);
+      _position = position;
+      _length = length;
+    }
+
+    public long length() {
+      return _length;
+    }
+
+    public InputStream getStream() throws IOException {
+      return new ByteArrayInputStream(getBytes(), _position, _length);
+    }
+
+    public void writeTo(OutputStream out) throws IOException {
+      out.write(getBytes(), _position, _length);
+    }
+
+    @Override
+    protected ToStringBuilder toString(ToStringBuilder sb) {
+      super.toString(sb);
+      if(_position >= 0) {
+        sb.append("content", ByteBuffer.wrap(_blob._bytes, _position, _length));
+      }
+      return sb;
+    } 
+  }
+
+  static abstract class EmbeddedPackageContentImpl 
+    extends EmbeddedContentImpl
+    implements PackageContent
+  {
+    private final String _prettyName;
+    private final String _className;
+    private final String _typeName;
+
+    protected EmbeddedPackageContentImpl(
+        OleBlobImpl blob, String prettyName, String className,
+        String typeName, int position, int length)
+    {
+      super(blob, position, length);
+      _prettyName = prettyName;
+      _className = className;
+      _typeName = typeName;
+    }
+
+    public String getPrettyName() {
+      return _prettyName;
+    }
+
+    public String getClassName() {
+      return _className;
+    }
+
+    public String getTypeName() {
+      return _typeName;
+    }
+
+    @Override
+    protected ToStringBuilder toString(ToStringBuilder sb) {
+      sb.append("prettyName", _prettyName)
+        .append("className", _className)
+        .append("typeName", _typeName);
+      super.toString(sb);
+      return sb;
+    } 
+  }
+
+  private static final class LinkContentImpl 
+    extends EmbeddedPackageContentImpl
+    implements LinkContent
+  {
+    private final String _fileName;
+    private final String _linkPath;
+    private final String _filePath;
+
+    private LinkContentImpl(OleBlobImpl blob, String prettyName,
+                            String className, String typeName,
+                            String fileName, String linkPath, 
+                            String filePath) 
+    {
+      super(blob, prettyName, className, typeName, -1, -1);
+      _fileName = fileName;
+      _linkPath = linkPath;
+      _filePath = filePath;      
+    }
+
+    public ContentType getType() {
+      return ContentType.LINK;
+    }
+
+    public String getFileName() {
+      return _fileName;
+    }
+
+    public String getLinkPath() {
+      return _linkPath;
+    }
+
+    public String getFilePath() {
+      return _filePath;
+    }
+
+    public InputStream getLinkStream() throws IOException {
+      return new FileInputStream(getLinkPath());
+    }
+
+    @Override
+    public String toString() {
+      return toString(CustomToStringStyle.builder(this))
+        .append("fileName", _fileName)
+        .append("linkPath", _linkPath)
+        .append("filePath", _filePath)
+        .toString();
+    }
+  }
+
+  private static final class SimplePackageContentImpl 
+    extends EmbeddedPackageContentImpl
+    implements SimplePackageContent
+  {
+    private final String _fileName;
+    private final String _filePath;
+    private final String _localFilePath;
+
+    private SimplePackageContentImpl(OleBlobImpl blob, String prettyName,
+                                     String className, String typeName,
+                                     int position, int length,
+                                     String fileName, String filePath,
+                                     String localFilePath) 
+    {
+      super(blob, prettyName, className, typeName, position, length);
+      _fileName = fileName;
+      _filePath = filePath;      
+      _localFilePath = localFilePath;
+    }
+
+    public ContentType getType() {
+      return ContentType.SIMPLE_PACKAGE;
+    }
+
+    public String getFileName() {
+      return _fileName;
+    }
+
+    public String getFilePath() {
+      return _filePath;
+    }
+
+    public String getLocalFilePath() {
+      return _localFilePath;
+    }
+
+    @Override
+    public String toString() {
+      return toString(CustomToStringStyle.builder(this))
+        .append("fileName", _fileName)
+        .append("filePath", _filePath)
+        .append("localFilePath", _localFilePath)
+        .toString();
+    }
+  }
+
+  private static final class OtherContentImpl 
+    extends EmbeddedPackageContentImpl
+    implements OtherContent
+  {
+    private OtherContentImpl(
+        OleBlobImpl blob, String prettyName, String className,
+        String typeName, int position, int length) 
+    {
+      super(blob, prettyName, className, typeName, position, length);
+    }        
+
+    public ContentType getType() {
+      return ContentType.OTHER;
+    }
+
+    @Override
+    public String toString() {
+      return toString(CustomToStringStyle.builder(this))
+        .toString();
+    }
+  }
+
+  private static final class UnknownContentImpl extends ContentImpl
+  {
+    private UnknownContentImpl(OleBlobImpl blob) {
+      super(blob);
+    }
+
+    public ContentType getType() {
+      return ContentType.UNKNOWN;
+    }
+
+    @Override
+    public String toString() {
+      return toString(CustomToStringStyle.builder(this))
+        .append("content", _blob._bytes)
+        .toString();
+    }
+  }
+  
+}
index 1c657664199a9cd45a62c933365e9fed13257d99..7e867cbf030f8145cb9b77a24ca580e73e78c27f 100644 (file)
@@ -35,6 +35,7 @@ import java.nio.channels.WritableByteChannel;
 
 import com.healthmarketscience.jackcess.Database;
 import com.healthmarketscience.jackcess.DatabaseBuilder;
+import com.healthmarketscience.jackcess.impl.ByteUtil;
 import com.healthmarketscience.jackcess.impl.DatabaseImpl;
 
 /**
@@ -116,13 +117,7 @@ public class MemFileChannel extends FileChannel
                             file, DatabaseImpl.RO_CHANNEL_MODE).getChannel(),
                         mode);
     } finally {
-      if(in != null) {
-        try {
-          in.close();
-        } catch(IOException e) {
-          // ignore close failure
-        }
-      }
+      ByteUtil.closeQuietly(in);
     }
   }
 
@@ -183,15 +178,20 @@ public class MemFileChannel extends FileChannel
   @Override
   public int read(ByteBuffer dst, long position) throws IOException {
     if(position >= _size) {
-      return  -1;
+      return -1;
     }
 
-    // we assume reads will always be within a single chunk (due to how mdb
-    // files work)
-    byte[] chunk = _data[getChunkIndex(position)];
-    int chunkOffset = getChunkOffset(position);
-    int numBytes = dst.remaining();
-    dst.put(chunk, chunkOffset, numBytes);
+    int numBytes = (int)Math.min(dst.remaining(), _size - position);
+    int rem = numBytes;
+
+    while(rem > 0) {
+      byte[] chunk = _data[getChunkIndex(position)];
+      int chunkOffset = getChunkOffset(position);
+      int bytesRead = Math.min(rem, CHUNK_SIZE - chunkOffset);
+      dst.put(chunk, chunkOffset, bytesRead);
+      rem -= bytesRead;
+      position += bytesRead;
+    }
 
     return numBytes;
   }
@@ -209,11 +209,16 @@ public class MemFileChannel extends FileChannel
     long newSize = position + numBytes;
     ensureCapacity(newSize);
 
-    // we assume writes will always be within a single chunk (due to how mdb
-    // files work)
-    byte[] chunk = _data[getChunkIndex(position)];
-    int chunkOffset = getChunkOffset(position);
-    src.get(chunk, chunkOffset, numBytes);
+    int rem = numBytes;
+    while(rem > 0) {
+      byte[] chunk = _data[getChunkIndex(position)];
+      int chunkOffset = getChunkOffset(position);
+      int bytesWritten = Math.min(rem, CHUNK_SIZE - chunkOffset);
+      src.get(chunk, chunkOffset, bytesWritten);
+      rem -= bytesWritten;
+      position += bytesWritten;
+    }
+
     if(newSize > _size) {
       _size = newSize;
     }
@@ -421,14 +426,25 @@ public class MemFileChannel extends FileChannel
   public long write(ByteBuffer[] srcs, int offset, int length)
     throws IOException
   {
-    throw new UnsupportedOperationException();
+    long numBytes = 0L;
+    for(int i = offset; i < offset + length; ++i) {
+      numBytes += write(srcs[i]);
+    }
+    return numBytes;
   }
 
   @Override
   public long read(ByteBuffer[] dsts, int offset, int length)
     throws IOException
-  {
-    throw new UnsupportedOperationException();
+  {    
+    long numBytes = 0L;
+    for(int i = offset; i < offset + length; ++i) {
+      if(_position >= _size) {
+        return ((numBytes > 0L) ? numBytes : -1L);
+      }
+      numBytes += read(dsts[i]);
+    }
+    return numBytes;
   }
 
   @Override
diff --git a/src/main/java/com/healthmarketscience/jackcess/util/OleBlob.java b/src/main/java/com/healthmarketscience/jackcess/util/OleBlob.java
new file mode 100644 (file)
index 0000000..54fdcca
--- /dev/null
@@ -0,0 +1,315 @@
+/*
+Copyright (c) 2013 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess.util;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.Blob;
+import java.util.List;
+
+import com.healthmarketscience.jackcess.impl.OleUtil;
+
+/**
+ *
+ * Note, implementation is read-only. 
+ *
+ * @author James Ahlborn
+ */
+public interface OleBlob extends Blob, Closeable
+{
+  /** Enum describing the types of blob contents which are currently
+      supported/understood */
+  public enum ContentType {
+    /** the blob contents are a link (file path) to some external content.
+        Content will be an instance LinkContent */
+    LINK, 
+    /** the blob contents are a simple wrapper around some embedded content
+        and related file names/paths.  Content will be an instance
+        SimplePackageContent */
+    SIMPLE_PACKAGE, 
+    /** the blob contents are a complex embedded data known as compound
+        storage (aka OLE2).  Working with compound storage requires the
+        optional POI library Content will be an instance CompoundContent.  If
+        the POI library is not available on the classpath, then compound
+        storage data will instead be returned as type {@link #OTHER}. */
+    COMPOUND_STORAGE,
+    /** the top-level blob wrapper is understood, but the nested blob contents
+        are unknown, probably just some embedded content.  Content will be an
+        instance of OtherContent */
+    OTHER,
+    /** the top-level blob wrapper is not understood (this may not be a valid
+        ole instance).  Content will simply be an instance of Content (the
+        data can be accessed from the main blob instance) */ 
+    UNKNOWN;
+  }
+
+  /**
+   * Writes the entire raw blob data to the given stream (this is the access
+   * db internal format, which includes all wrapper information).
+   *
+   * @param out stream to which the blob will be written
+   */
+  public void writeTo(OutputStream out) throws IOException;
+
+  /**
+   * Returns the decoded form of the blob contents, if understandable.
+   */
+  public Content getContent() throws IOException;
+
+
+  public interface Content 
+  {    
+    /**
+     * Returns the type of this content.
+     */
+    public ContentType getType();
+
+    /**
+     * Returns the blob which owns this content.
+     */
+    public OleBlob getBlob();
+  }
+
+  public interface PackageContent extends Content
+  {    
+    public String getPrettyName() throws IOException;
+
+    public String getClassName() throws IOException;
+
+    public String getTypeName() throws IOException;
+  }
+
+  public interface EmbeddedContent extends Content
+  {
+    public long length();
+
+    public InputStream getStream() throws IOException;
+
+    public void writeTo(OutputStream out) throws IOException;    
+  }
+
+  public interface LinkContent extends PackageContent
+  {
+    public String getFileName();
+
+    public String getLinkPath();
+
+    public String getFilePath();
+
+    public InputStream getLinkStream() throws IOException;
+  }
+
+  public interface SimplePackageContent 
+    extends PackageContent, EmbeddedContent
+  {
+    public String getFileName();
+
+    public String getFilePath();
+
+    public String getLocalFilePath();
+  }
+
+  public interface CompoundContent extends PackageContent, EmbeddedContent
+  {
+    public List<String> getEntries() throws IOException;
+
+    public InputStream getEntryStream(String entryName) throws IOException;
+
+    public boolean hasContentsEntry() throws IOException;
+
+    public InputStream getContentsEntryStream() throws IOException;
+  }  
+
+  public interface OtherContent extends PackageContent, EmbeddedContent
+  {
+  }
+
+  /**
+   * Builder style class for constructing an OleBlob. The {@link
+   * #fromInternalData} method can be used for interpreting existing ole data.
+   * <p/>
+   * Example for creating new ole data:
+   * <pre>
+   *   OleBlob oleBlob = new OleBlob.Builder()
+   *     .setSimplePackageFile(contentFile)
+   *     .toBlob();
+   * </pre>
+   */
+  public class Builder
+  {
+    public static final String PACKAGE_PRETTY_NAME = "Packager Shell Object";
+    public static final String PACKAGE_TYPE_NAME = "Package";
+
+    private ContentType _type;
+    private byte[] _bytes;
+    private InputStream _stream;
+    private long _contentLen;
+    private String _fileName;
+    private String _filePath;
+    private String _prettyName;
+    private String _className;
+    private String _typeName;
+    
+    public ContentType getType() {
+      return _type;
+    }
+
+    public byte[] getBytes() {
+      return _bytes;
+    }
+
+    public InputStream getStream() {
+      return _stream;
+    }
+
+    public long getContentLength() {
+      return _contentLen;
+    }
+
+    public String getFileName() {
+      return _fileName;
+    }
+
+    public String getFilePath() {
+      return _filePath;
+    }
+
+    public String getPrettyName() {
+      return _prettyName;
+    }
+
+    public String getClassName() {
+      return _className;
+    }
+    
+    public String getTypeName() {
+      return _typeName;
+    }
+    
+    public Builder setSimplePackageBytes(byte[] bytes) {
+      _bytes = bytes;
+      _contentLen = bytes.length;
+      setDefaultPackageType();
+      _type = ContentType.SIMPLE_PACKAGE;
+      return this;
+    }
+
+    public Builder setSimplePackageStream(InputStream in, long length) {
+      _stream = in;
+      _contentLen = length;
+      setDefaultPackageType();
+      _type = ContentType.SIMPLE_PACKAGE;
+      return this;
+    }
+
+    public Builder setSimplePackageFileName(String fileName) {
+      _fileName = fileName;
+      setDefaultPackageType();
+      _type = ContentType.SIMPLE_PACKAGE;
+      return this;
+    }
+
+    public Builder setSimplePackageFilePath(String filePath) {
+      _filePath = filePath;
+      setDefaultPackageType();
+      _type = ContentType.SIMPLE_PACKAGE;
+      return this;
+    }
+
+    public Builder setSimplePackage(File f) throws FileNotFoundException {
+      _fileName = f.getName();
+      _filePath = f.getPath();
+      return setSimplePackageStream(new FileInputStream(f), f.length());
+    }
+
+    public Builder setLinkFileName(String fileName) {
+      _fileName = fileName;
+      setDefaultPackageType();
+      _type = ContentType.LINK;
+      return this;
+    }
+
+    public Builder setLinkPath(String link) {
+      _filePath = link;
+      setDefaultPackageType();
+      _type = ContentType.LINK;
+      return this;
+    }
+
+    public Builder setLink(File f) {
+      _fileName = f.getName();
+      _filePath = f.getPath();
+      setDefaultPackageType();
+      _type = ContentType.LINK;
+      return this;
+    }
+
+    private void setDefaultPackageType() {
+      if(_prettyName == null) {
+        _prettyName = PACKAGE_PRETTY_NAME;
+      }
+      if(_className == null) {
+        _className = PACKAGE_TYPE_NAME;
+      }
+    }
+
+    public Builder setOtherBytes(byte[] bytes) {
+      _bytes = bytes;
+      _contentLen = bytes.length;
+      _type = ContentType.OTHER;
+      return this;
+    }
+
+    public Builder setOtherStream(InputStream in, long length) {
+      _stream = in;
+      _contentLen = length;
+      _type = ContentType.OTHER;
+      return this;
+    }
+
+    public Builder setPackagePrettyName(String prettyName) {
+      _prettyName = prettyName;
+      return this;
+    }
+
+    public Builder setPackageClassName(String className) {
+      _className = className;
+      return this;
+    }
+
+    public Builder setPackageTypeName(String typeName) {
+      _typeName = typeName;
+      return this;
+    }
+
+    public OleBlob toBlob() throws IOException {
+      return OleUtil.createBlob(this);
+    }
+
+    public static OleBlob fromInternalData(byte[] bytes) throws IOException {
+      return OleUtil.parseBlob(bytes);
+    }
+  }
+}