From: James Ahlborn Date: Thu, 12 Sep 2013 12:20:16 +0000 (+0000) Subject: initial support for blob creation/parsing X-Git-Tag: jackcess-2.0.1~24 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=966b10b933bc789209bc11a5372cd6f7eb2ded95;p=jackcess.git initial support for blob creation/parsing git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@797 f203690c-595d-4dc9-a70b-905162fa7fd2 --- diff --git a/pom.xml b/pom.xml index 155c147..ce22f5b 100644 --- a/pom.xml +++ b/pom.xml @@ -224,7 +224,16 @@ junit 4.11 test - + + + + + org.apache.poi + poi + 3.9 + true + + diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 339fde8..a4a33f4 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -4,6 +4,11 @@ Tim McCune + + + Add initial support for creating/parsing ole content. + + Brand new API! This release is not backwards compatible with 1.x diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 1a71d95..0f9714d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -1709,6 +1709,8 @@ public class ColumnImpl implements Column, Comparable { 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 index 0000000..bcf3255 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java @@ -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. + *

+ * 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 getEntries() throws IOException { + return getEntries(new ArrayList(), 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 entryNames = new ArrayList(); + for(String str : entryName.split(ENTRY_SEPARATOR)) { + if(str.length() == 0) { + continue; + } + entryNames.add(decodeEntryName(str)); + } + + DirectoryEntry dir = getFileSystem().getRoot(); + DocumentEntry entry = null; + Iterator 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 getEntries(List 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(), getFileSystem().getRoot(), + ENTRY_SEPARATOR, true)); + } catch(IOException e) { + sb.append("entries", "<" + e + ">"); + } + + return sb.toString(); + } + } + +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java index f6b4ade..8247f4f 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java @@ -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 index 0000000..b82ded2 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java @@ -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 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(); + } + } + +} diff --git a/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java b/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java index 1c65766..7e867cb 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java @@ -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 index 0000000..54fdcca --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/util/OleBlob.java @@ -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 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. + *

+ * Example for creating new ole data: + *

+   *   OleBlob oleBlob = new OleBlob.Builder()
+   *     .setSimplePackageFile(contentFile)
+   *     .toBlob();
+   * 
+ */ + 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); + } + } +}