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
This commit is contained in:
Nick Burch 2011-03-03 12:41:39 +00:00
parent 145de166a1
commit aa1963cd3b
11 changed files with 388 additions and 34 deletions

View File

@ -18,12 +18,16 @@
package org.apache.poi.hmef; package org.apache.poi.hmef;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import org.apache.poi.hmef.attribute.MAPIAttribute; 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.TNEFAttribute;
import org.apache.poi.hmef.attribute.TNEFDateAttribute;
import org.apache.poi.hmef.attribute.TNEFMAPIAttribute; import org.apache.poi.hmef.attribute.TNEFMAPIAttribute;
import org.apache.poi.hmef.attribute.TNEFProperty; import org.apache.poi.hmef.attribute.TNEFProperty;
import org.apache.poi.hmef.attribute.TNEFStringAttribute;
import org.apache.poi.hsmf.datatypes.MAPIProperty; import org.apache.poi.hsmf.datatypes.MAPIProperty;
@ -87,8 +91,68 @@ public final class Attachment {
return mapiAttributes; 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() { public String getFilename() {
TNEFAttribute attr = null; return getString(TNEFProperty.ID_ATTACHTITLE);
return null; }
/**
* 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();
} }
} }

View File

@ -155,19 +155,7 @@ public final class HMEFMessage {
* if it isn't set * if it isn't set
*/ */
private String getString(MAPIProperty id) { private String getString(MAPIProperty id) {
MAPIAttribute attr = getMessageMAPIAttribute(id); return MAPIStringAttribute.getAsString( 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;
} }
/** /**

View File

@ -165,6 +165,8 @@ public class MAPIAttribute {
MAPIAttribute attr; MAPIAttribute attr;
if(type == Types.UNICODE_STRING || type == Types.ASCII_STRING) { if(type == Types.UNICODE_STRING || type == Types.ASCII_STRING) {
attr = new MAPIStringAttribute(prop, type, data); 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) { } else if(id == MAPIProperty.RTF_COMPRESSED.id) {
attr = new MAPIRtfAttribute(prop, type, data); attr = new MAPIRtfAttribute(prop, type, data);
} else { } else {

View File

@ -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;
}
}

View File

@ -63,4 +63,22 @@ public final class MAPIStringAttribute extends MAPIAttribute {
public String toString() { public String toString() {
return getProperty().toString() + " " + data; 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;
}
} }

View File

@ -70,6 +70,9 @@ public class TNEFAttribute {
type == TNEFProperty.TYPE_TEXT) { type == TNEFProperty.TYPE_TEXT) {
return new TNEFStringAttribute(id, type, inp); return new TNEFStringAttribute(id, type, inp);
} }
if(type == TNEFProperty.TYPE_DATE) {
return new TNEFDateAttribute(id, type, inp);
}
return new TNEFAttribute(id, type, inp); return new TNEFAttribute(id, type, inp);
} }

View File

@ -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;
}
}

View File

@ -25,31 +25,57 @@ import org.apache.poi.hmef.HMEFMessage;
import org.apache.poi.util.StringUtil; 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. * or one of its {@link Attachment}s.
*/ */
public final class TNEFStringAttribute extends TNEFAttribute { public final class TNEFStringAttribute extends TNEFAttribute {
private String data;
/** /**
* Constructs a single new string attribute from the id, type, * Constructs a single new string attribute from the id, type,
* and the contents of the stream * and the contents of the stream
*/ */
protected TNEFStringAttribute(int id, int type, InputStream inp) throws IOException { protected TNEFStringAttribute(int id, int type, InputStream inp) throws IOException {
super(id, type, inp); super(id, type, inp);
String tmpData = null;
byte[] data = getData();
if(getType() == TNEFProperty.TYPE_TEXT) {
tmpData = StringUtil.getFromUnicodeLE(data);
} else {
tmpData = 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() { public String getString() {
byte[] data = getData(); return this.data;
// TODO Verify if these are the right way around
if(getType() == TNEFProperty.TYPE_TEXT) {
return StringUtil.getFromUnicodeLE(data);
}
return StringUtil.getFromCompressedUnicode(
data, 0, data.length
);
} }
public String toString() { public String toString() {
return "Attribute " + getProperty().toString() + ", type=" + getType() + return "Attribute " + getProperty().toString() + ", type=" + getType() +
", data=" + getString(); ", 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;
}
} }

View File

@ -25,7 +25,9 @@ import java.util.List;
import org.apache.poi.hmef.HMEFMessage; import org.apache.poi.hmef.HMEFMessage;
import org.apache.poi.hmef.attribute.TNEFAttribute; import org.apache.poi.hmef.attribute.TNEFAttribute;
import org.apache.poi.hmef.attribute.MAPIAttribute; 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.TNEFProperty;
import org.apache.poi.hmef.attribute.TNEFStringAttribute;
import org.apache.poi.util.HexDump; import org.apache.poi.util.HexDump;
import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndian;
@ -108,6 +110,14 @@ public final class HMEFDumper {
// Print the contents // Print the contents
String indent = " "; 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); System.out.println(indent + "Data of length " + attr.getData().length);
if(attr.getData().length > 0) { if(attr.getData().length > 0) {
int len = attr.getData().length; int len = attr.getData().length;

View File

@ -17,30 +17,67 @@
package org.apache.poi.hmef; 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 junit.framework.TestCase;
import org.apache.poi.POIDataSamples; import org.apache.poi.POIDataSamples;
import org.apache.poi.util.IOUtils;
public final class TestAttachments extends TestCase { public final class TestAttachments extends TestCase {
private static final POIDataSamples _samples = POIDataSamples.getHMEFInstance(); 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 * Check the file is as we expect
*/ */
public void testCounts() throws Exception { public void testCounts() throws Exception {
HMEFMessage msg = new HMEFMessage(
_samples.openResourceAsStream("quick-winmail.dat")
);
// Should have 5 attachments // Should have 5 attachments
assertEquals(5, msg.getAttachments().size()); assertEquals(5, quick.getAttachments().size());
} }
/** /**
* Check some basic bits about the attachments * Check some basic bits about the attachments
*/ */
public void testBasicAttachments() throws Exception { public void testBasicAttachments() throws Exception {
// TODO List<Attachment> 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 * the right values for key things
*/ */
public void testAttachmentDetails() throws Exception { public void testAttachmentDetails() throws Exception {
// TODO List<Attachment> 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 * Ensure the attachment contents come back as they should do
*/ */
public void testAttachmentContents() throws Exception { public void testAttachmentContents() throws Exception {
// TODO List<Attachment> 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<expected.length; i++) {
assertEquals("Byte " + i + " wrong", expected[i], actual[i]);
}
} }
} }

View File

@ -154,9 +154,15 @@ public final class POIDataSamples {
+ "' not found in data dir '" + _resolvedDataDir.getAbsolutePath() + "'"); + "' not found in data dir '" + _resolvedDataDir.getAbsolutePath() + "'");
} }
try { try {
if(sampleFileName.length() > 0 && !sampleFileName.equals(f.getCanonicalFile().getName())){ if(sampleFileName.length() > 0) {
throw new RuntimeException("File name is case-sensitive: requested '" + sampleFileName 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() + "'"); + "' but actual file is '" + f.getCanonicalFile().getName() + "'");
}
} }
} catch (IOException e){ } catch (IOException e){
throw new RuntimeException(e); throw new RuntimeException(e);