git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@810 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-2.0.1
import java.io.Closeable; | import java.io.Closeable; | ||||
import java.io.FileWriter; | import java.io.FileWriter; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | |||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import java.io.PrintWriter; | import java.io.PrintWriter; | ||||
import java.nio.ByteBuffer; | import java.nio.ByteBuffer; | ||||
return newArr; | return newArr; | ||||
} | } | ||||
/** | |||||
* Copies the given InputStream to the given OutputStream. | |||||
*/ | |||||
public static void copy(InputStream in, OutputStream out) throws IOException { | |||||
byte[] buf = new byte[8 * 1024]; | |||||
int read = 0; | |||||
while((read = in.read(buf)) > -1) { | |||||
out.write(buf, 0, read); | |||||
} | |||||
} | |||||
/** | /** | ||||
* Closes the given Closeable if non-null, swallows any IOExceptions. | * Closes the given Closeable if non-null, swallows any IOExceptions. | ||||
*/ | */ |
ByteArrayOutputStream bout = new ByteArrayOutputStream(); | ByteArrayOutputStream bout = new ByteArrayOutputStream(); | ||||
if(value instanceof InputStream) { | if(value instanceof InputStream) { | ||||
byte[] buf = new byte[8 * 1024]; | |||||
InputStream in = (InputStream)value; | |||||
int read = 0; | |||||
while((read = in.read(buf)) != -1) { | |||||
bout.write(buf, 0, read); | |||||
} | |||||
ByteUtil.copy((InputStream)value, bout); | |||||
} else { | } else { | ||||
// if all else fails, serialize it | // if all else fails, serialize it | ||||
ObjectOutputStream oos = new ObjectOutputStream(bout); | ObjectOutputStream oos = new ObjectOutputStream(bout); |
import java.io.FileNotFoundException; | import java.io.FileNotFoundException; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.OutputStream; | |||||
import java.io.UnsupportedEncodingException; | import java.io.UnsupportedEncodingException; | ||||
import java.net.URLDecoder; | import java.net.URLDecoder; | ||||
import java.net.URLEncoder; | import java.net.URLEncoder; | ||||
import java.util.Iterator; | import java.util.Iterator; | ||||
import java.util.List; | import java.util.List; | ||||
import com.healthmarketscience.jackcess.RuntimeIOException; | |||||
import static com.healthmarketscience.jackcess.impl.OleUtil.*; | import static com.healthmarketscience.jackcess.impl.OleUtil.*; | ||||
import com.healthmarketscience.jackcess.util.MemFileChannel; | import com.healthmarketscience.jackcess.util.MemFileChannel; | ||||
import static com.healthmarketscience.jackcess.util.OleBlob.*; | import static com.healthmarketscience.jackcess.util.OleBlob.*; | ||||
import org.apache.poi.poifs.filesystem.DirectoryEntry; | import org.apache.poi.poifs.filesystem.DirectoryEntry; | ||||
import org.apache.poi.poifs.filesystem.DocumentEntry; | import org.apache.poi.poifs.filesystem.DocumentEntry; | ||||
import org.apache.poi.poifs.filesystem.DocumentInputStream; | import org.apache.poi.poifs.filesystem.DocumentInputStream; | ||||
import org.apache.poi.poifs.filesystem.Entry; | |||||
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; | import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; | ||||
/** | /** | ||||
* support in OleUtil can be utilized without requiring POI. | * support in OleUtil can be utilized without requiring POI. | ||||
* | * | ||||
* @author James Ahlborn | * @author James Ahlborn | ||||
* @usage _advanced_class_ | |||||
*/ | */ | ||||
public class CompoundOleUtil implements OleUtil.CompoundPackageFactory | public class CompoundOleUtil implements OleUtil.CompoundPackageFactory | ||||
{ | { | ||||
{ | { | ||||
} | } | ||||
/** | |||||
* Creates a nes CompoundContent for the given blob information. | |||||
*/ | |||||
public ContentImpl createCompoundPackageContent( | public ContentImpl createCompoundPackageContent( | ||||
OleBlobImpl blob, String prettyName, String className, String typeName, | OleBlobImpl blob, String prettyName, String className, String typeName, | ||||
ByteBuffer blobBb, int dataBlockLen) | ByteBuffer blobBb, int dataBlockLen) | ||||
blobBb.position(), dataBlockLen); | blobBb.position(), dataBlockLen); | ||||
} | } | ||||
/** | |||||
* Gets a DocumentEntry from compound storage based on a fully qualified, | |||||
* encoded entry name. | |||||
* | |||||
* @param entryName fully qualified, encoded entry name | |||||
* @param dir root directory of the compound storage | |||||
* | |||||
* @return the relevant DocumentEntry | |||||
* @throws FileNotFoundException if the entry does not exist | |||||
* @throws IOException if some other io error occurs | |||||
*/ | |||||
public static DocumentEntry getDocumentEntry(String entryName, | |||||
DirectoryEntry dir) | |||||
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)); | |||||
} | |||||
DocumentEntry entry = null; | |||||
Iterator<String> iter = entryNames.iterator(); | |||||
while(iter.hasNext()) { | |||||
org.apache.poi.poifs.filesystem.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 static String encodeEntryName(String name) { | private static String encodeEntryName(String name) { | ||||
try { | try { | ||||
return URLEncoder.encode(name, ENTRY_NAME_CHARSET); | return URLEncoder.encode(name, ENTRY_NAME_CHARSET); | ||||
return _fs; | return _fs; | ||||
} | } | ||||
public List<String> getEntries() throws IOException { | |||||
return getEntries(new ArrayList<String>(), getFileSystem().getRoot(), | |||||
ENTRY_SEPARATOR, false); | |||||
public Iterator<Entry> iterator() { | |||||
try { | |||||
return getEntries(new ArrayList<Entry>(), getFileSystem().getRoot(), | |||||
ENTRY_SEPARATOR).iterator(); | |||||
} catch(IOException e) { | |||||
throw new RuntimeIOException(e); | |||||
} | |||||
} | } | ||||
public InputStream getEntryStream(String entryName) throws IOException { | |||||
return new DocumentInputStream(getDocumentEntry(entryName)); | |||||
public EntryImpl getEntry(String entryName) throws IOException { | |||||
return new EntryImpl(entryName, | |||||
getDocumentEntry(entryName, getFileSystem().getRoot())); | |||||
} | } | ||||
public boolean hasContentsEntry() throws IOException { | public boolean hasContentsEntry() throws IOException { | ||||
return getFileSystem().getRoot().hasEntry(CONTENTS_ENTRY); | 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; | |||||
public EntryImpl getContentsEntry() throws IOException { | |||||
return getEntry(CONTENTS_ENTRY); | |||||
} | } | ||||
private List<String> getEntries(List<String> entries, DirectoryEntry dir, | |||||
String prefix, boolean includeDetails) { | |||||
for(Entry entry : dir) { | |||||
private List<Entry> getEntries(List<Entry> entries, DirectoryEntry dir, | |||||
String prefix) { | |||||
for(org.apache.poi.poifs.filesystem.Entry entry : dir) { | |||||
if (entry instanceof DirectoryEntry) { | if (entry instanceof DirectoryEntry) { | ||||
// .. recurse into this directory | // .. recurse into this directory | ||||
getEntries(entries, (DirectoryEntry)entry, prefix + ENTRY_SEPARATOR, | |||||
includeDetails); | |||||
getEntries(entries, (DirectoryEntry)entry, prefix + ENTRY_SEPARATOR); | |||||
} else if(entry instanceof DocumentEntry) { | } else if(entry instanceof DocumentEntry) { | ||||
// grab the entry name/detils | // grab the entry name/detils | ||||
DocumentEntry de = (DocumentEntry)entry; | |||||
String entryName = prefix + encodeEntryName(entry.getName()); | String entryName = prefix + encodeEntryName(entry.getName()); | ||||
if(includeDetails) { | |||||
entryName += " (" + ((DocumentEntry)entry).getSize() + ")"; | |||||
} | |||||
entries.add(entryName); | |||||
entries.add(new EntryImpl(entryName, de)); | |||||
} | } | ||||
} | } | ||||
return entries; | return entries; | ||||
try { | try { | ||||
sb.append("hasContentsEntry", hasContentsEntry()); | sb.append("hasContentsEntry", hasContentsEntry()); | ||||
sb.append("entries", | |||||
getEntries(new ArrayList<String>(), getFileSystem().getRoot(), | |||||
ENTRY_SEPARATOR, true)); | |||||
sb.append("entries", getEntries(new ArrayList<Entry>(), | |||||
getFileSystem().getRoot(), | |||||
ENTRY_SEPARATOR)); | |||||
} catch(IOException e) { | } catch(IOException e) { | ||||
sb.append("entries", "<" + e + ">"); | sb.append("entries", "<" + e + ">"); | ||||
} | } | ||||
return sb.toString(); | return sb.toString(); | ||||
} | } | ||||
private final class EntryImpl implements CompoundContent.Entry | |||||
{ | |||||
private final String _name; | |||||
private final DocumentEntry _docEntry; | |||||
private EntryImpl(String name, DocumentEntry docEntry) { | |||||
_name = name; | |||||
_docEntry = docEntry; | |||||
} | |||||
public ContentType getType() { | |||||
return ContentType.UNKNOWN; | |||||
} | |||||
public String getName() { | |||||
return _name; | |||||
} | |||||
public CompoundContentImpl getParent() { | |||||
return CompoundContentImpl.this; | |||||
} | |||||
public OleBlobImpl getBlob() { | |||||
return getParent().getBlob(); | |||||
} | |||||
public long length() { | |||||
return _docEntry.getSize(); | |||||
} | |||||
public InputStream getStream() throws IOException { | |||||
return new DocumentInputStream(_docEntry); | |||||
} | |||||
public void writeTo(OutputStream out) throws IOException { | |||||
InputStream in = null; | |||||
try { | |||||
ByteUtil.copy(in = getStream(), out); | |||||
} finally { | |||||
ByteUtil.closeQuietly(in); | |||||
} | |||||
} | |||||
@Override | |||||
public String toString() { | |||||
return CustomToStringStyle.valueBuilder(this) | |||||
.append("name", _name) | |||||
.append("length", length()) | |||||
.toString(); | |||||
} | |||||
} | |||||
} | } | ||||
} | } |
* Utility code for working with OLE data. | * Utility code for working with OLE data. | ||||
* | * | ||||
* @author James Ahlborn | * @author James Ahlborn | ||||
* @usage _advanced_class_ | |||||
*/ | */ | ||||
public class OleUtil | public class OleUtil | ||||
{ | { |
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import java.sql.Blob; | import java.sql.Blob; | ||||
import java.util.List; | |||||
import com.healthmarketscience.jackcess.impl.OleUtil; | import com.healthmarketscience.jackcess.impl.OleUtil; | ||||
* ContentType#COMPOUND_STORAGE} type. Compound storage is a complex | * ContentType#COMPOUND_STORAGE} type. Compound storage is a complex | ||||
* embedding format also known as OLE2. In some situations (mostly | * embedding format also known as OLE2. In some situations (mostly | ||||
* non-microsoft office file formats) the actual content is available from | * non-microsoft office file formats) the actual content is available from | ||||
* the {@link #getContentsEntryStream} method (if {@link #hasContentsEntry} | |||||
* the {@link #getContentsEntry} method (if {@link #hasContentsEntry} | |||||
* returns {@code true}). In other situations (e.g. microsoft office file | * returns {@code true}). In other situations (e.g. microsoft office file | ||||
* formats), the actual content is most or all of the compound content (but | * formats), the actual content is most or all of the compound content (but | ||||
* retrieving the final file may be a complex operation, beyond the scope of | |||||
* retrieving the final file may be a complex operation beyond the scope of | |||||
* jackcess). Note that the CompoundContent type will only be available if | * jackcess). Note that the CompoundContent type will only be available if | ||||
* the POI library is in the classpath, otherwise compound content will be | * the POI library is in the classpath, otherwise compound content will be | ||||
* returned as OtherContent. | * returned as OtherContent. | ||||
*/ | */ | ||||
public interface CompoundContent extends PackageContent, EmbeddedContent | |||||
public interface CompoundContent extends PackageContent, EmbeddedContent, | |||||
Iterable<CompoundContent.Entry> | |||||
{ | { | ||||
public List<String> getEntries() throws IOException; | |||||
public InputStream getEntryStream(String entryName) throws IOException; | |||||
public Entry getEntry(String entryName) throws IOException; | |||||
public boolean hasContentsEntry() throws IOException; | public boolean hasContentsEntry() throws IOException; | ||||
public InputStream getContentsEntryStream() throws IOException; | |||||
public Entry getContentsEntry() throws IOException; | |||||
/** | |||||
* A document entry in the compound storage. | |||||
*/ | |||||
public interface Entry extends EmbeddedContent | |||||
{ | |||||
public String getName(); | |||||
/** | |||||
* Returns the CompoundContent which owns this entry. | |||||
*/ | |||||
public CompoundContent getParent(); | |||||
} | |||||
} | } | ||||
/** | /** |
PROMOTION("testPromotion"), | PROMOTION("testPromotion"), | ||||
COMPLEX("complexDataTest"), | COMPLEX("complexDataTest"), | ||||
UNSUPPORTED("unsupportedFieldsTest"), | UNSUPPORTED("unsupportedFieldsTest"), | ||||
LINKED("linkerTest"); | |||||
LINKED("linkerTest"), | |||||
BLOB("testOle"); | |||||
private final String _basename; | private final String _basename; | ||||
package com.healthmarketscience.jackcess.util; | package com.healthmarketscience.jackcess.util; | ||||
import java.io.File; | import java.io.File; | ||||
import java.io.FileNotFoundException; | |||||
import java.io.FileOutputStream; | |||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import com.healthmarketscience.jackcess.ColumnBuilder; | import com.healthmarketscience.jackcess.ColumnBuilder; | ||||
import com.healthmarketscience.jackcess.Row; | import com.healthmarketscience.jackcess.Row; | ||||
import com.healthmarketscience.jackcess.Table; | import com.healthmarketscience.jackcess.Table; | ||||
import com.healthmarketscience.jackcess.TableBuilder; | import com.healthmarketscience.jackcess.TableBuilder; | ||||
import com.healthmarketscience.jackcess.complex.Attachment; | |||||
import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; | |||||
import com.healthmarketscience.jackcess.impl.ByteUtil; | import com.healthmarketscience.jackcess.impl.ByteUtil; | ||||
import com.healthmarketscience.jackcess.impl.CompoundOleUtil; | |||||
import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; | import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; | ||||
import junit.framework.TestCase; | import junit.framework.TestCase; | ||||
import org.apache.poi.poifs.filesystem.DocumentEntry; | |||||
import org.apache.poi.poifs.filesystem.DocumentInputStream; | |||||
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; | |||||
/** | /** | ||||
* | * | ||||
db.close(); | db.close(); | ||||
} | } | ||||
} | } | ||||
public void testReadBlob() throws Exception | |||||
{ | |||||
for(TestDB testDb : TestDB.getSupportedForBasename(Basename.BLOB, true)) { | |||||
Database db = open(testDb); | |||||
Table t = db.getTable("Table1"); | |||||
for(Row row : t) { | |||||
OleBlob oleBlob = null; | |||||
try { | |||||
String name = (String)row.get("name"); | |||||
oleBlob = OleBlob.Builder.fromInternalData((byte[])row.get("ole_data")); | |||||
OleBlob.Content content = oleBlob.getContent(); | |||||
Attachment attach = null; | |||||
if(content.getType() != OleBlob.ContentType.LINK) { | |||||
attach = ((ComplexValueForeignKey)row.get("attach_data")) | |||||
.getAttachments().get(0); | |||||
} | |||||
switch(content.getType()) { | |||||
case LINK: | |||||
OleBlob.LinkContent lc = (OleBlob.LinkContent)content; | |||||
if("test_link".equals(name)) { | |||||
assertEquals("Z:\\jackcess_test\\ole\\test_data.txt", lc.getLinkPath()); | |||||
} else { | |||||
assertEquals("Z:\\jackcess_test\\ole\\test_datau2.txt", lc.getLinkPath()); | |||||
} | |||||
break; | |||||
case SIMPLE_PACKAGE: | |||||
OleBlob.SimplePackageContent spc = (OleBlob.SimplePackageContent)content; | |||||
byte[] packageBytes = toByteArray(spc.getStream(), spc.length()); | |||||
assertTrue(Arrays.equals(attach.getFileData(), packageBytes)); | |||||
break; | |||||
case COMPOUND_STORAGE: | |||||
OleBlob.CompoundContent cc = (OleBlob.CompoundContent)content; | |||||
if(cc.hasContentsEntry()) { | |||||
OleBlob.CompoundContent.Entry entry = cc.getContentsEntry(); | |||||
byte[] entryBytes = toByteArray(entry.getStream(), entry.length()); | |||||
assertTrue(Arrays.equals(attach.getFileData(), entryBytes)); | |||||
} else { | |||||
if("test_word.doc".equals(name)) { | |||||
checkCompoundEntries(cc, | |||||
"/%02OlePres000", 466, | |||||
"/WordDocument", 4096, | |||||
"/%05SummaryInformation", 4096, | |||||
"/%05DocumentSummaryInformation", 4096, | |||||
"/%03AccessObjSiteData", 56, | |||||
"/%02OlePres001", 1620, | |||||
"/1Table", 6380, | |||||
"/%01CompObj", 114, | |||||
"/%01Ole", 20); | |||||
checkCompoundStorage(cc, attach); | |||||
} else if("test_excel.xls".equals(name)) { | |||||
checkCompoundEntries(cc, | |||||
"/%02OlePres000", 1326, | |||||
"/%03AccessObjSiteData", 56, | |||||
"/%05SummaryInformation", 200, | |||||
"/%05DocumentSummaryInformation", 264, | |||||
"/%02OlePres001", 4208, | |||||
"/%01CompObj", 107, | |||||
"/Workbook", 13040, | |||||
"/%01Ole", 20); | |||||
// the excel data seems to be modified when embedded as ole, | |||||
// so we can't reallly test it against the attachment data | |||||
} else { | |||||
throw new RuntimeException("unexpected compound entry " + name); | |||||
} | |||||
} | |||||
break; | |||||
case OTHER: | |||||
OleBlob.OtherContent oc = (OleBlob.OtherContent)content; | |||||
byte[] otherBytes = toByteArray(oc.getStream(), oc.length()); | |||||
assertTrue(Arrays.equals(attach.getFileData(), otherBytes)); | |||||
break; | |||||
default: | |||||
throw new RuntimeException("unexpected type " + content.getType()); | |||||
} | |||||
} finally { | |||||
ByteUtil.closeQuietly(oleBlob); | |||||
} | |||||
} | |||||
db.close(); | |||||
} | |||||
} | |||||
private static void checkCompoundEntries(OleBlob.CompoundContent cc, | |||||
Object... entryInfo) | |||||
throws Exception | |||||
{ | |||||
int idx = 0; | |||||
for(OleBlob.CompoundContent.Entry e : cc) { | |||||
String entryName = (String)entryInfo[idx]; | |||||
int entryLen = (Integer)entryInfo[idx + 1]; | |||||
assertEquals(entryName, e.getName()); | |||||
assertEquals(entryLen, e.length()); | |||||
idx += 2; | |||||
} | |||||
} | |||||
private static void checkCompoundStorage(OleBlob.CompoundContent cc, | |||||
Attachment attach) | |||||
throws Exception | |||||
{ | |||||
File tmpData = File.createTempFile("attach_", ".dat"); | |||||
try { | |||||
FileOutputStream fout = new FileOutputStream(tmpData); | |||||
fout.write(attach.getFileData()); | |||||
fout.close(); | |||||
NPOIFSFileSystem attachFs = new NPOIFSFileSystem(tmpData, true); | |||||
for(OleBlob.CompoundContent.Entry e : cc) { | |||||
DocumentEntry attachE = null; | |||||
try { | |||||
attachE = CompoundOleUtil.getDocumentEntry(e.getName(), attachFs.getRoot()); | |||||
} catch(FileNotFoundException fnfe) { | |||||
// ignored, the ole data has extra entries | |||||
continue; | |||||
} | |||||
byte[] attachEBytes = toByteArray(new DocumentInputStream(attachE), | |||||
attachE.getSize()); | |||||
byte[] entryBytes = toByteArray(e.getStream(), e.length()); | |||||
assertTrue(Arrays.equals(attachEBytes, entryBytes)); | |||||
} | |||||
ByteUtil.closeQuietly(attachFs); | |||||
} finally { | |||||
tmpData.delete(); | |||||
} | |||||
} | |||||
} | } |