From: Nick Burch Date: Thu, 3 Mar 2011 12:41:39 +0000 (+0000) Subject: Improve HMEF handling of typed attributes (Strings and Dates), for both TNEF and... X-Git-Tag: REL_3_8_BETA2~53 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=9ae939c6f799b058f88b8b3ca7617d1ae9fdf600;p=poi.git Improve HMEF handling of typed attributes (Strings and Dates), for both TNEF and MAPI attributes, and use this to allow easier access to common file parts. Then use this in the attachment unit tests. git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1076603 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/scratchpad/src/org/apache/poi/hmef/Attachment.java b/src/scratchpad/src/org/apache/poi/hmef/Attachment.java index 1d74ccc749..e961481a1e 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/Attachment.java +++ b/src/scratchpad/src/org/apache/poi/hmef/Attachment.java @@ -18,12 +18,16 @@ package org.apache.poi.hmef; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.apache.poi.hmef.attribute.MAPIAttribute; +import org.apache.poi.hmef.attribute.MAPIStringAttribute; import org.apache.poi.hmef.attribute.TNEFAttribute; +import org.apache.poi.hmef.attribute.TNEFDateAttribute; import org.apache.poi.hmef.attribute.TNEFMAPIAttribute; import org.apache.poi.hmef.attribute.TNEFProperty; +import org.apache.poi.hmef.attribute.TNEFStringAttribute; import org.apache.poi.hsmf.datatypes.MAPIProperty; @@ -87,8 +91,68 @@ public final class Attachment { return mapiAttributes; } + + /** + * Return the string value of the mapi property, or null + * if it isn't set + */ + private String getString(MAPIProperty id) { + return MAPIStringAttribute.getAsString( getMessageMAPIAttribute(id) ); + } + /** + * Returns the string value of the TNEF property, or + * null if it isn't set + */ + private String getString(TNEFProperty id) { + return TNEFStringAttribute.getAsString( getMessageAttribute(id) ); + } + + /** + * Returns the short filename + */ public String getFilename() { - TNEFAttribute attr = null; - return null; + return getString(TNEFProperty.ID_ATTACHTITLE); + } + /** + * Returns the long filename + */ + public String getLongFilename() { + return getString(MAPIProperty.ATTACH_LONG_FILENAME); + } + /** + * Returns the file extension + */ + public String getExtension() { + return getString(MAPIProperty.ATTACH_EXTENSION); + } + + /** + * Return when the file was last modified, if known. + */ + public Date getModifiedDate() { + return TNEFDateAttribute.getAsDate( + getMessageAttribute(TNEFProperty.ID_ATTACHMODIFYDATE) + ); + } + + /** + * Returns the contents of the attachment. + */ + public byte[] getContents() { + TNEFAttribute contents = getMessageAttribute(TNEFProperty.ID_ATTACHDATA); + if(contents == null) { + throw new IllegalArgumentException("Attachment corrupt - no Data section"); + } + return contents.getData(); + } + + /** + * Returns the Meta File rendered representation + * of the attachment, or null if not set. + */ + public byte[] getRenderedMetaFile() { + TNEFAttribute meta = getMessageAttribute(TNEFProperty.ID_ATTACHMETAFILE); + if(meta == null) return null; + return meta.getData(); } } diff --git a/src/scratchpad/src/org/apache/poi/hmef/HMEFMessage.java b/src/scratchpad/src/org/apache/poi/hmef/HMEFMessage.java index c23718ee68..7da39f9c6c 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/HMEFMessage.java +++ b/src/scratchpad/src/org/apache/poi/hmef/HMEFMessage.java @@ -155,19 +155,7 @@ public final class HMEFMessage { * if it isn't set */ private String getString(MAPIProperty id) { - MAPIAttribute attr = getMessageMAPIAttribute(id); - if(id == null) { - return null; - } - if(attr instanceof MAPIStringAttribute) { - return ((MAPIStringAttribute)attr).getDataString(); - } - if(attr instanceof MAPIRtfAttribute) { - return ((MAPIRtfAttribute)attr).getDataString(); - } - - System.err.println("Warning, no string property found: " + attr.toString()); - return null; + return MAPIStringAttribute.getAsString( getMessageMAPIAttribute(id) ); } /** diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIAttribute.java index 1ebd121144..31c3cbb885 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIAttribute.java +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIAttribute.java @@ -165,6 +165,8 @@ public class MAPIAttribute { MAPIAttribute attr; if(type == Types.UNICODE_STRING || type == Types.ASCII_STRING) { attr = new MAPIStringAttribute(prop, type, data); + } else if(type == Types.APP_TIME || type == Types.TIME) { + attr = new MAPIDateAttribute(prop, type, data); } else if(id == MAPIProperty.RTF_COMPRESSED.id) { attr = new MAPIRtfAttribute(prop, type, data); } else { diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIDateAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIDateAttribute.java new file mode 100644 index 0000000000..bf786366d1 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIDateAttribute.java @@ -0,0 +1,70 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hmef.attribute; + +import java.util.Date; + +import org.apache.poi.hmef.Attachment; +import org.apache.poi.hmef.HMEFMessage; +import org.apache.poi.hpsf.Util; +import org.apache.poi.hsmf.datatypes.MAPIProperty; +import org.apache.poi.util.LittleEndian; + +/** + * A pure-MAPI attribute holding a Date, which applies + * to a {@link HMEFMessage} or one of its {@link Attachment}s. + */ +public final class MAPIDateAttribute extends MAPIAttribute { + private Date data; + + /** + * Constructs a single new date attribute from the id, type, + * and the contents of the stream + */ + protected MAPIDateAttribute(MAPIProperty property, int type, byte[] data) { + super(property, type, data); + + // The value is a 64 bit Windows Filetime + this.data = Util.filetimeToDate( + LittleEndian.getLong(data, 0) + ); + } + + public Date getDate() { + return this.data; + } + + public String toString() { + return getProperty().toString() + " " + data.toString(); + } + + /** + * Returns the Date of a Attribute, converting as appropriate + */ + public static Date getAsDate(MAPIAttribute attr) { + if(attr == null) { + return null; + } + if(attr instanceof MAPIDateAttribute) { + return ((MAPIDateAttribute)attr).getDate(); + } + + System.err.println("Warning, non date property found: " + attr.toString()); + return null; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIStringAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIStringAttribute.java index e7550de736..d53e59c7d5 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIStringAttribute.java +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIStringAttribute.java @@ -63,4 +63,22 @@ public final class MAPIStringAttribute extends MAPIAttribute { public String toString() { return getProperty().toString() + " " + data; } + + /** + * Returns the string of a Attribute, converting as appropriate + */ + public static String getAsString(MAPIAttribute attr) { + if(attr == null) { + return null; + } + if(attr instanceof MAPIStringAttribute) { + return ((MAPIStringAttribute)attr).getDataString(); + } + if(attr instanceof MAPIRtfAttribute) { + return ((MAPIRtfAttribute)attr).getDataString(); + } + + System.err.println("Warning, non string property found: " + attr.toString()); + return null; + } } diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFAttribute.java index b2152a5e34..af0ac3dcba 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFAttribute.java +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFAttribute.java @@ -70,6 +70,9 @@ public class TNEFAttribute { type == TNEFProperty.TYPE_TEXT) { return new TNEFStringAttribute(id, type, inp); } + if(type == TNEFProperty.TYPE_DATE) { + return new TNEFDateAttribute(id, type, inp); + } return new TNEFAttribute(id, type, inp); } diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFDateAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFDateAttribute.java new file mode 100644 index 0000000000..70793899f4 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFDateAttribute.java @@ -0,0 +1,91 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hmef.attribute; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +import org.apache.poi.hmef.Attachment; +import org.apache.poi.hmef.HMEFMessage; +import org.apache.poi.hpsf.Util; +import org.apache.poi.util.LittleEndian; + +/** + * A Date attribute which applies to a {@link HMEFMessage} + * or one of its {@link Attachment}s. + */ +public final class TNEFDateAttribute extends TNEFAttribute { + private Date data; + + /** + * Constructs a single new date attribute from the id, type, + * and the contents of the stream + */ + protected TNEFDateAttribute(int id, int type, InputStream inp) throws IOException { + super(id, type, inp); + + byte[] data = getData(); + if(data.length == 8) { + // The value is a 64 bit Windows Filetime + this.data = Util.filetimeToDate( + LittleEndian.getLong(getData(), 0) + ); + } else if(data.length == 14) { + // It's the 7 date fields. We think it's in UTC... + Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + c.set(Calendar.YEAR, LittleEndian.getUShort(data, 0)); + c.set(Calendar.MONTH, LittleEndian.getUShort(data, 2) - 1); // Java months are 0 based! + c.set(Calendar.DAY_OF_MONTH, LittleEndian.getUShort(data, 4)); + c.set(Calendar.HOUR_OF_DAY, LittleEndian.getUShort(data, 6)); + c.set(Calendar.MINUTE, LittleEndian.getUShort(data, 8)); + c.set(Calendar.SECOND, LittleEndian.getUShort(data, 10)); + // The 7th field is day of week, which we don't require + c.set(Calendar.MILLISECOND, 0); // Not set in the file + this.data = c.getTime(); + } else { + throw new IllegalArgumentException("Invalid date, found " + data.length + " bytes"); + } + } + + public Date getDate() { + return this.data; + } + + public String toString() { + return "Attribute " + getProperty().toString() + ", type=" + getType() + + ", date=" + data.toString(); + } + + /** + * Returns the Date of a Attribute, converting as appropriate + */ + public static Date getAsDate(TNEFAttribute attr) { + if(attr == null) { + return null; + } + if(attr instanceof TNEFDateAttribute) { + return ((TNEFDateAttribute)attr).getDate(); + } + + System.err.println("Warning, non date property found: " + attr.toString()); + return null; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFStringAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFStringAttribute.java index 6063c5e2c0..983d3f9a23 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFStringAttribute.java +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFStringAttribute.java @@ -25,31 +25,57 @@ import org.apache.poi.hmef.HMEFMessage; import org.apache.poi.util.StringUtil; /** - * An String attribute which applies to a {@link HMEFMessage} + * A String attribute which applies to a {@link HMEFMessage} * or one of its {@link Attachment}s. */ public final class TNEFStringAttribute extends TNEFAttribute { + private String data; + /** * Constructs a single new string attribute from the id, type, * and the contents of the stream */ protected TNEFStringAttribute(int id, int type, InputStream inp) throws IOException { super(id, type, inp); - } - - public String getString() { + + String tmpData = null; byte[] data = getData(); - // TODO Verify if these are the right way around if(getType() == TNEFProperty.TYPE_TEXT) { - return StringUtil.getFromUnicodeLE(data); + tmpData = StringUtil.getFromUnicodeLE(data); + } else { + tmpData = StringUtil.getFromCompressedUnicode( + data, 0, data.length + ); } - return StringUtil.getFromCompressedUnicode( - data, 0, data.length - ); + + // Strip off the null terminator if present + if(tmpData.endsWith("\0")) { + tmpData = tmpData.substring(0, tmpData.length()-1); + } + this.data = tmpData; + } + + public String getString() { + return this.data; } public String toString() { return "Attribute " + getProperty().toString() + ", type=" + getType() + ", data=" + getString(); } + + /** + * Returns the string of a Attribute, converting as appropriate + */ + public static String getAsString(TNEFAttribute attr) { + if(attr == null) { + return null; + } + if(attr instanceof TNEFStringAttribute) { + return ((TNEFStringAttribute)attr).getString(); + } + + System.err.println("Warning, non string property found: " + attr.toString()); + return null; + } } diff --git a/src/scratchpad/src/org/apache/poi/hmef/dev/HMEFDumper.java b/src/scratchpad/src/org/apache/poi/hmef/dev/HMEFDumper.java index e1ad196ee8..7addb6b84e 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/dev/HMEFDumper.java +++ b/src/scratchpad/src/org/apache/poi/hmef/dev/HMEFDumper.java @@ -25,7 +25,9 @@ import java.util.List; import org.apache.poi.hmef.HMEFMessage; import org.apache.poi.hmef.attribute.TNEFAttribute; import org.apache.poi.hmef.attribute.MAPIAttribute; +import org.apache.poi.hmef.attribute.TNEFDateAttribute; import org.apache.poi.hmef.attribute.TNEFProperty; +import org.apache.poi.hmef.attribute.TNEFStringAttribute; import org.apache.poi.util.HexDump; import org.apache.poi.util.LittleEndian; @@ -108,6 +110,14 @@ public final class HMEFDumper { // Print the contents String indent = " "; + + if(attr instanceof TNEFStringAttribute) { + System.out.println(indent + indent + indent + ((TNEFStringAttribute)attr).getString()); + } + if(attr instanceof TNEFDateAttribute) { + System.out.println(indent + indent + indent + ((TNEFDateAttribute)attr).getDate()); + } + System.out.println(indent + "Data of length " + attr.getData().length); if(attr.getData().length > 0) { int len = attr.getData().length; diff --git a/src/scratchpad/testcases/org/apache/poi/hmef/TestAttachments.java b/src/scratchpad/testcases/org/apache/poi/hmef/TestAttachments.java index f5ddb8aeb0..13a82024a0 100644 --- a/src/scratchpad/testcases/org/apache/poi/hmef/TestAttachments.java +++ b/src/scratchpad/testcases/org/apache/poi/hmef/TestAttachments.java @@ -17,30 +17,67 @@ package org.apache.poi.hmef; +import java.io.IOException; +import java.text.DateFormat; +import java.util.List; +import java.util.Locale; + import junit.framework.TestCase; import org.apache.poi.POIDataSamples; +import org.apache.poi.util.IOUtils; public final class TestAttachments extends TestCase { private static final POIDataSamples _samples = POIDataSamples.getHMEFInstance(); + private HMEFMessage quick; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + quick = new HMEFMessage( + _samples.openResourceAsStream("quick-winmail.dat") + ); + } /** * Check the file is as we expect */ public void testCounts() throws Exception { - HMEFMessage msg = new HMEFMessage( - _samples.openResourceAsStream("quick-winmail.dat") - ); - // Should have 5 attachments - assertEquals(5, msg.getAttachments().size()); + assertEquals(5, quick.getAttachments().size()); } /** * Check some basic bits about the attachments */ public void testBasicAttachments() throws Exception { - // TODO + List attachments = quick.getAttachments(); + + // Word first + assertEquals("quick.doc", attachments.get(0).getFilename()); + assertEquals("quick.doc", attachments.get(0).getLongFilename()); + assertEquals(".doc", attachments.get(0).getExtension()); + + // Then HTML + assertEquals("QUICK~1.HTM", attachments.get(1).getFilename()); + assertEquals("quick.html", attachments.get(1).getLongFilename()); + assertEquals(".html", attachments.get(1).getExtension()); + + // Then PDF + assertEquals("quick.pdf", attachments.get(2).getFilename()); + assertEquals("quick.pdf", attachments.get(2).getLongFilename()); + assertEquals(".pdf", attachments.get(2).getExtension()); + + // Then Text + assertEquals("quick.txt", attachments.get(3).getFilename()); + assertEquals("quick.txt", attachments.get(3).getLongFilename()); + assertEquals(".txt", attachments.get(3).getExtension()); + + // And finally XML + assertEquals("quick.xml", attachments.get(4).getFilename()); + assertEquals("quick.xml", attachments.get(4).getLongFilename()); + assertEquals(".xml", attachments.get(4).getExtension()); } /** @@ -48,13 +85,52 @@ public final class TestAttachments extends TestCase { * the right values for key things */ public void testAttachmentDetails() throws Exception { - // TODO + List attachments = quick.getAttachments(); + + DateFormat fmt = DateFormat.getDateTimeInstance( + DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.UK + ); + + // They should all have the same date on them + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(0).getModifiedDate())); + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(1).getModifiedDate())); + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(2).getModifiedDate())); + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(3).getModifiedDate())); + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(4).getModifiedDate())); + + // They should all have a 3512 byte metafile rendered version + assertEquals(3512, attachments.get(0).getRenderedMetaFile().length); + assertEquals(3512, attachments.get(1).getRenderedMetaFile().length); + assertEquals(3512, attachments.get(2).getRenderedMetaFile().length); + assertEquals(3512, attachments.get(3).getRenderedMetaFile().length); + assertEquals(3512, attachments.get(4).getRenderedMetaFile().length); } /** * Ensure the attachment contents come back as they should do */ public void testAttachmentContents() throws Exception { - // TODO + List attachments = quick.getAttachments(); + + assertContents("quick.doc", attachments.get(0)); + assertContents("quick.html", attachments.get(1)); + assertContents("quick.pdf", attachments.get(2)); + assertContents("quick.txt", attachments.get(3)); + assertContents("quick.xml", attachments.get(4)); + } + + private void assertContents(String filename, Attachment attachment) + throws IOException { + assertEquals(filename, attachment.getLongFilename()); + + byte[] expected = IOUtils.toByteArray( + _samples.openResourceAsStream("quick-contents/" + filename) + ); + byte[] actual = attachment.getContents(); + + assertEquals(expected.length, actual.length); + for(int i=0; i 0 && !sampleFileName.equals(f.getCanonicalFile().getName())){ - throw new RuntimeException("File name is case-sensitive: requested '" + sampleFileName + if(sampleFileName.length() > 0) { + String fn = sampleFileName; + if(fn.indexOf('/') > 0) { + fn = fn.substring(fn.indexOf('/')+1); + } + if(!fn.equals(f.getCanonicalFile().getName())){ + throw new RuntimeException("File name is case-sensitive: requested '" + fn + "' but actual file is '" + f.getCanonicalFile().getName() + "'"); + } } } catch (IOException e){ throw new RuntimeException(e);