git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1773544 13f79535-47bb-0310-9956-ffa450edef68pull/44/head
@@ -34,134 +34,132 @@ import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
/** | |||
* Collection of convenience chunks for standard parts of the MSG file attachment. | |||
* Collection of convenience chunks for standard parts of the MSG file | |||
* attachment. | |||
*/ | |||
public class AttachmentChunks implements ChunkGroup { | |||
private static POILogger logger = POILogFactory.getLogger(AttachmentChunks.class); | |||
public static final String PREFIX = "__attach_version1.0_#"; | |||
public ByteChunk attachData; | |||
public StringChunk attachExtension; | |||
public StringChunk attachFileName; | |||
public StringChunk attachLongFileName; | |||
public StringChunk attachMimeTag; | |||
public DirectoryChunk attachmentDirectory; | |||
/** | |||
* This is in WMF Format. You'll probably want to pass it | |||
* to Apache Batik to turn it into a SVG that you can | |||
* then display. | |||
*/ | |||
public ByteChunk attachRenderingWMF; | |||
/** | |||
* What the POIFS name of this attachment is. | |||
*/ | |||
private String poifsName; | |||
/** Holds all the chunks that were found. */ | |||
private List<Chunk> allChunks = new ArrayList<Chunk>(); | |||
public AttachmentChunks(String poifsName) { | |||
this.poifsName = poifsName; | |||
} | |||
/** | |||
* Is this Attachment an embedded MAPI message? | |||
*/ | |||
public boolean isEmbeddedMessage() { | |||
return (attachmentDirectory != null); | |||
} | |||
/** | |||
* Returns the embedded MAPI message, if the attachment | |||
* is an embedded message, or null otherwise | |||
*/ | |||
public MAPIMessage getEmbeddedMessage() throws IOException { | |||
if (attachmentDirectory != null) { | |||
return attachmentDirectory.getAsEmbededMessage(); | |||
} | |||
return null; | |||
} | |||
/** | |||
* Returns the embedded object, if the attachment is an | |||
* object based embedding (image, document etc), or null | |||
* if it's an embedded message | |||
*/ | |||
public byte[] getEmbeddedAttachmentObject() { | |||
if (attachData != null) { | |||
return attachData.getValue(); | |||
} | |||
return null; | |||
} | |||
public Chunk[] getAll() { | |||
return allChunks.toArray(new Chunk[allChunks.size()]); | |||
} | |||
public Chunk[] getChunks() { | |||
return getAll(); | |||
} | |||
public String getPOIFSName() { | |||
return poifsName; | |||
} | |||
/** | |||
* Called by the parser whenever a chunk is found. | |||
*/ | |||
public void record(Chunk chunk) { | |||
// TODO: add further members for other properties like: | |||
// - ATTACH_ADDITIONAL_INFO | |||
// - ATTACH_CONTENT_BASE | |||
// - ATTACH_CONTENT_LOCATION | |||
// - ATTACH_DISPOSITION | |||
// - ATTACH_ENCODING | |||
// - ATTACH_FLAGS | |||
// - ATTACH_LONG_PATHNAME | |||
// - ATTACH_SIZE | |||
final int chunkId = chunk.getChunkId(); | |||
if (chunkId == ATTACH_DATA.id) { | |||
if(chunk instanceof ByteChunk) { | |||
attachData = (ByteChunk)chunk; | |||
} else if(chunk instanceof DirectoryChunk) { | |||
attachmentDirectory = (DirectoryChunk)chunk; | |||
} else { | |||
logger.log(POILogger.ERROR, "Unexpected data chunk of type " + chunk); | |||
} | |||
} else if(chunkId == ATTACH_EXTENSION.id) { | |||
attachExtension = (StringChunk)chunk; | |||
} else if(chunkId == ATTACH_FILENAME.id) { | |||
attachFileName = (StringChunk)chunk; | |||
} else if(chunkId == ATTACH_LONG_FILENAME.id) { | |||
attachLongFileName = (StringChunk)chunk; | |||
} else if(chunkId == ATTACH_MIME_TAG.id) { | |||
attachMimeTag = (StringChunk)chunk; | |||
} else if(chunkId == ATTACH_RENDERING.id) { | |||
attachRenderingWMF = (ByteChunk)chunk; | |||
} | |||
// And add to the main list | |||
allChunks.add(chunk); | |||
} | |||
/** | |||
* Used to flag that all the chunks of the attachment | |||
* have now been located. | |||
*/ | |||
public void chunksComplete() { | |||
// Currently, we don't need to do anything special once | |||
// all the chunks have been located | |||
} | |||
/** | |||
* Orders by the attachment number. | |||
*/ | |||
public static class AttachmentChunksSorter implements Comparator<AttachmentChunks>, Serializable { | |||
public int compare(AttachmentChunks a, AttachmentChunks b) { | |||
return a.poifsName.compareTo(b.poifsName); | |||
} | |||
} | |||
private static POILogger logger = POILogFactory.getLogger(AttachmentChunks.class); | |||
public static final String PREFIX = "__attach_version1.0_#"; | |||
public ByteChunk attachData; | |||
public StringChunk attachExtension; | |||
public StringChunk attachFileName; | |||
public StringChunk attachLongFileName; | |||
public StringChunk attachMimeTag; | |||
public DirectoryChunk attachmentDirectory; | |||
/** | |||
* This is in WMF Format. You'll probably want to pass it to Apache Batik to | |||
* turn it into a SVG that you can then display. | |||
*/ | |||
public ByteChunk attachRenderingWMF; | |||
/** | |||
* What the POIFS name of this attachment is. | |||
*/ | |||
private String poifsName; | |||
/** Holds all the chunks that were found. */ | |||
private List<Chunk> allChunks = new ArrayList<Chunk>(); | |||
public AttachmentChunks(String poifsName) { | |||
this.poifsName = poifsName; | |||
} | |||
/** | |||
* Is this Attachment an embedded MAPI message? | |||
*/ | |||
public boolean isEmbeddedMessage() { | |||
return (attachmentDirectory != null); | |||
} | |||
/** | |||
* Returns the embedded MAPI message, if the attachment is an embedded | |||
* message, or null otherwise | |||
*/ | |||
public MAPIMessage getEmbeddedMessage() throws IOException { | |||
if (attachmentDirectory != null) { | |||
return attachmentDirectory.getAsEmbededMessage(); | |||
} | |||
return null; | |||
} | |||
/** | |||
* Returns the embedded object, if the attachment is an object based | |||
* embedding (image, document etc), or null if it's an embedded message | |||
*/ | |||
public byte[] getEmbeddedAttachmentObject() { | |||
if (attachData != null) { | |||
return attachData.getValue(); | |||
} | |||
return null; | |||
} | |||
public Chunk[] getAll() { | |||
return allChunks.toArray(new Chunk[allChunks.size()]); | |||
} | |||
public Chunk[] getChunks() { | |||
return getAll(); | |||
} | |||
public String getPOIFSName() { | |||
return poifsName; | |||
} | |||
/** | |||
* Called by the parser whenever a chunk is found. | |||
*/ | |||
public void record(Chunk chunk) { | |||
// TODO: add further members for other properties like: | |||
// - ATTACH_ADDITIONAL_INFO | |||
// - ATTACH_CONTENT_BASE | |||
// - ATTACH_CONTENT_LOCATION | |||
// - ATTACH_DISPOSITION | |||
// - ATTACH_ENCODING | |||
// - ATTACH_FLAGS | |||
// - ATTACH_LONG_PATHNAME | |||
// - ATTACH_SIZE | |||
final int chunkId = chunk.getChunkId(); | |||
if (chunkId == ATTACH_DATA.id) { | |||
if (chunk instanceof ByteChunk) { | |||
attachData = (ByteChunk) chunk; | |||
} else if (chunk instanceof DirectoryChunk) { | |||
attachmentDirectory = (DirectoryChunk) chunk; | |||
} else { | |||
logger.log(POILogger.ERROR, "Unexpected data chunk of type " + chunk); | |||
} | |||
} else if (chunkId == ATTACH_EXTENSION.id) { | |||
attachExtension = (StringChunk) chunk; | |||
} else if (chunkId == ATTACH_FILENAME.id) { | |||
attachFileName = (StringChunk) chunk; | |||
} else if (chunkId == ATTACH_LONG_FILENAME.id) { | |||
attachLongFileName = (StringChunk) chunk; | |||
} else if (chunkId == ATTACH_MIME_TAG.id) { | |||
attachMimeTag = (StringChunk) chunk; | |||
} else if (chunkId == ATTACH_RENDERING.id) { | |||
attachRenderingWMF = (ByteChunk) chunk; | |||
} | |||
// And add to the main list | |||
allChunks.add(chunk); | |||
} | |||
/** | |||
* Used to flag that all the chunks of the attachment have now been located. | |||
*/ | |||
public void chunksComplete() { | |||
// Currently, we don't need to do anything special once | |||
// all the chunks have been located | |||
} | |||
/** | |||
* Orders by the attachment number. | |||
*/ | |||
public static class AttachmentChunksSorter | |||
implements Comparator<AttachmentChunks>, Serializable { | |||
public int compare(AttachmentChunks a, AttachmentChunks b) { | |||
return a.poifsName.compareTo(b.poifsName); | |||
} | |||
} | |||
} |
@@ -24,89 +24,87 @@ import org.apache.poi.hsmf.datatypes.Types.MAPIType; | |||
import org.apache.poi.util.IOUtils; | |||
/** | |||
* A Chunk that holds binary data, normally unparsed. | |||
* Generally as we know how to make sense of the | |||
* contents, we create a new Chunk class and add | |||
* a special case in the parser for them. | |||
* A Chunk that holds binary data, normally unparsed. Generally as we know how | |||
* to make sense of the contents, we create a new Chunk class and add a special | |||
* case in the parser for them. | |||
*/ | |||
public class ByteChunk extends Chunk { | |||
private byte[] value; | |||
/** | |||
* Creates a Byte Chunk. | |||
*/ | |||
public ByteChunk(String namePrefix, int chunkId, MAPIType type) { | |||
super(namePrefix, chunkId, type); | |||
} | |||
/** | |||
* Create a Byte Chunk, with the specified | |||
* type. | |||
*/ | |||
public ByteChunk(int chunkId, MAPIType type) { | |||
super(chunkId, type); | |||
} | |||
public void readValue(InputStream value) throws IOException { | |||
this.value = IOUtils.toByteArray(value); | |||
} | |||
public void writeValue(OutputStream out) throws IOException { | |||
out.write(value); | |||
} | |||
public byte[] getValue() { | |||
return value; | |||
} | |||
public void setValue(byte[] value) { | |||
this.value = value; | |||
} | |||
/** | |||
* Returns the data in a debug-friendly string format | |||
*/ | |||
public String toString() { | |||
return toDebugFriendlyString(value); | |||
} | |||
/** | |||
* Formats the byte array in a debug-friendly way, | |||
* showing all of a short array, and the start of a | |||
* longer one. | |||
*/ | |||
protected static String toDebugFriendlyString(byte[] value) { | |||
if (value == null) | |||
return "(Null Byte Array)"; | |||
StringBuffer text = new StringBuffer(); | |||
text.append("Bytes len=").append(value.length); | |||
text.append(" ["); | |||
int limit = Math.min(value.length, 16); | |||
if (value.length > 16) { | |||
limit = 12; | |||
} | |||
for (int i=0; i<limit; i++) { | |||
if (i > 0) | |||
text.append(','); | |||
text.append(value[i]); | |||
} | |||
if (value.length > 16) { | |||
text.append(",...."); | |||
} | |||
text.append("]"); | |||
return text.toString(); | |||
} | |||
/** | |||
* Returns the data, formatted as a string assuming it | |||
* was a non-unicode string. | |||
* If your data isn't in fact stored as basically | |||
* ASCII, don't expect this to return much of any | |||
* sense.... | |||
* @return the data formatted as a string | |||
*/ | |||
public String getAs7bitString() { | |||
return StringChunk.parseAs7BitData(value); | |||
} | |||
private byte[] value; | |||
/** | |||
* Creates a Byte Chunk. | |||
*/ | |||
public ByteChunk(String namePrefix, int chunkId, MAPIType type) { | |||
super(namePrefix, chunkId, type); | |||
} | |||
/** | |||
* Create a Byte Chunk, with the specified type. | |||
*/ | |||
public ByteChunk(int chunkId, MAPIType type) { | |||
super(chunkId, type); | |||
} | |||
public void readValue(InputStream value) throws IOException { | |||
this.value = IOUtils.toByteArray(value); | |||
} | |||
public void writeValue(OutputStream out) throws IOException { | |||
out.write(value); | |||
} | |||
public byte[] getValue() { | |||
return value; | |||
} | |||
public void setValue(byte[] value) { | |||
this.value = value; | |||
} | |||
/** | |||
* Returns the data in a debug-friendly string format | |||
*/ | |||
public String toString() { | |||
return toDebugFriendlyString(value); | |||
} | |||
/** | |||
* Formats the byte array in a debug-friendly way, showing all of a short | |||
* array, and the start of a longer one. | |||
*/ | |||
protected static String toDebugFriendlyString(byte[] value) { | |||
if (value == null) | |||
return "(Null Byte Array)"; | |||
StringBuffer text = new StringBuffer(); | |||
text.append("Bytes len=").append(value.length); | |||
text.append(" ["); | |||
int limit = Math.min(value.length, 16); | |||
if (value.length > 16) { | |||
limit = 12; | |||
} | |||
for (int i = 0; i < limit; i++) { | |||
if (i > 0) | |||
text.append(','); | |||
text.append(value[i]); | |||
} | |||
if (value.length > 16) { | |||
text.append(",...."); | |||
} | |||
text.append("]"); | |||
return text.toString(); | |||
} | |||
/** | |||
* Returns the data, formatted as a string assuming it was a non-unicode | |||
* string. If your data isn't in fact stored as basically ASCII, don't | |||
* expect this to return much of any sense.... | |||
* | |||
* @return the data formatted as a string | |||
*/ | |||
public String getAs7bitString() { | |||
return StringChunk.parseAs7BitData(value); | |||
} | |||
} |
@@ -25,55 +25,59 @@ import java.util.Locale; | |||
import org.apache.poi.hsmf.datatypes.Types.MAPIType; | |||
public abstract class Chunk { | |||
public static final String DEFAULT_NAME_PREFIX = "__substg1.0_"; | |||
protected int chunkId; | |||
protected MAPIType type; | |||
protected String namePrefix; | |||
protected Chunk(String namePrefix, int chunkId, MAPIType type) { | |||
this.namePrefix = namePrefix; | |||
this.chunkId = chunkId; | |||
this.type = type; | |||
} | |||
protected Chunk(int chunkId, MAPIType type) { | |||
this(DEFAULT_NAME_PREFIX, chunkId, type); | |||
} | |||
/** | |||
* Gets the id of this chunk | |||
*/ | |||
public int getChunkId() { | |||
return this.chunkId; | |||
} | |||
/** | |||
* Gets the numeric type of this chunk. | |||
*/ | |||
public MAPIType getType() { | |||
return this.type; | |||
} | |||
/** | |||
* Creates a string to use to identify this chunk in the POI file system object. | |||
*/ | |||
public String getEntryName() { | |||
String type = this.type.asFileEnding(); | |||
String chunkId = Integer.toHexString(this.chunkId); | |||
while(chunkId.length() < 4) chunkId = "0" + chunkId; | |||
return this.namePrefix + chunkId.toUpperCase(Locale.ROOT) | |||
+ type.toUpperCase(Locale.ROOT); | |||
} | |||
/** | |||
* Writes the value of this chunk back out again. | |||
*/ | |||
public abstract void writeValue(OutputStream out) throws IOException; | |||
/** | |||
* Reads the value of this chunk using an InputStream | |||
*/ | |||
public abstract void readValue(InputStream value) throws IOException; | |||
public static final String DEFAULT_NAME_PREFIX = "__substg1.0_"; | |||
protected int chunkId; | |||
protected MAPIType type; | |||
protected String namePrefix; | |||
protected Chunk(String namePrefix, int chunkId, MAPIType type) { | |||
this.namePrefix = namePrefix; | |||
this.chunkId = chunkId; | |||
this.type = type; | |||
} | |||
protected Chunk(int chunkId, MAPIType type) { | |||
this(DEFAULT_NAME_PREFIX, chunkId, type); | |||
} | |||
/** | |||
* Gets the id of this chunk | |||
*/ | |||
public int getChunkId() { | |||
return this.chunkId; | |||
} | |||
/** | |||
* Gets the numeric type of this chunk. | |||
*/ | |||
public MAPIType getType() { | |||
return this.type; | |||
} | |||
/** | |||
* Creates a string to use to identify this chunk in the POI file system | |||
* object. | |||
*/ | |||
public String getEntryName() { | |||
String type = this.type.asFileEnding(); | |||
String chunkId = Integer.toHexString(this.chunkId); | |||
while (chunkId.length() < 4) | |||
chunkId = "0" + chunkId; | |||
return this.namePrefix | |||
+ chunkId.toUpperCase(Locale.ROOT) | |||
+ type.toUpperCase(Locale.ROOT); | |||
} | |||
/** | |||
* Writes the value of this chunk back out again. | |||
*/ | |||
public abstract void writeValue(OutputStream out) throws IOException; | |||
/** | |||
* Reads the value of this chunk using an InputStream | |||
*/ | |||
public abstract void readValue(InputStream value) throws IOException; | |||
} |
@@ -17,28 +17,26 @@ | |||
package org.apache.poi.hsmf.datatypes; | |||
/** | |||
* A variable length {@link PropertyValue} that is | |||
* backed by a {@link Chunk} | |||
* A variable length {@link PropertyValue} that is backed by a {@link Chunk} | |||
* TODO Provide a way to link these up with the chunks | |||
*/ | |||
public class ChunkBasedPropertyValue extends PropertyValue { | |||
public ChunkBasedPropertyValue(MAPIProperty property, long flags, byte[] offsetData) { | |||
super(property, flags, offsetData); | |||
} | |||
public ChunkBasedPropertyValue(MAPIProperty property, long flags, byte[] offsetData) { | |||
super(property, flags, offsetData); | |||
} | |||
@Override | |||
public Chunk getValue() { | |||
// TODO Decode the value into an offset | |||
// TODO Look up the chunk based on that | |||
return null; | |||
} | |||
@Override | |||
public Chunk getValue() { | |||
// TODO Decode the value into an offset | |||
// TODO Look up the chunk based on that | |||
return null; | |||
} | |||
/** | |||
* Stores the offset of the chunk as the property value | |||
*/ | |||
public void setValue(Chunk chunk) { | |||
// TODO | |||
} | |||
/** | |||
* Stores the offset of the chunk as the property value | |||
*/ | |||
public void setValue(Chunk chunk) { | |||
// TODO | |||
} | |||
} |
@@ -18,15 +18,13 @@ | |||
package org.apache.poi.hsmf.datatypes; | |||
/** | |||
* A group of chunks, that are at the same point in the | |||
* file structure. | |||
* A group of chunks, that are at the same point in the file structure. | |||
*/ | |||
public interface ChunkGroup { | |||
/** | |||
* Returns the chunks that make up the group. | |||
* Should certainly contain all the interesting Chunks, | |||
* but needn't always contain all of the Chunks. | |||
*/ | |||
/** | |||
* Returns the chunks that make up the group. Should certainly contain all | |||
* the interesting Chunks, but needn't always contain all of the Chunks. | |||
*/ | |||
public Chunk[] getChunks(); | |||
/** |
@@ -21,18 +21,15 @@ import java.util.List; | |||
import java.util.Map; | |||
/** | |||
* A group of chunks which is indexable by {@link MAPIProperty} | |||
* entries. | |||
* A group of chunks which is indexable by {@link MAPIProperty} entries. | |||
*/ | |||
public interface ChunkGroupWithProperties extends ChunkGroup { | |||
/** | |||
* Returns all the Properties contained in the Chunk, along | |||
* with their Values. | |||
* Normally, each property will have one value, sometimes | |||
* none, and rarely multiple (normally for Unknown etc). | |||
* For fixed sized properties, the value can be fetched | |||
* straight from the {@link PropertyValue}. For variable | |||
* sized properties, you'll need to go via the chunk. | |||
*/ | |||
public Map<MAPIProperty,List<PropertyValue>> getProperties(); | |||
/** | |||
* Returns all the Properties contained in the Chunk, along with their | |||
* Values. Normally, each property will have one value, sometimes none, and | |||
* rarely multiple (normally for Unknown etc). For fixed sized properties, | |||
* the value can be fetched straight from the {@link PropertyValue}. For | |||
* variable sized properties, you'll need to go via the chunk. | |||
*/ | |||
public Map<MAPIProperty, List<PropertyValue>> getProperties(); | |||
} |
@@ -26,169 +26,158 @@ import java.util.Map; | |||
import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
/** | |||
* Collection of convenience chunks for standard parts of the MSG file. | |||
* | |||
* Not all of these will be present in any given file. | |||
* | |||
* A partial list is available at: | |||
* http://msdn.microsoft.com/en-us/library/ms526356%28v=exchg.10%29.aspx | |||
* | |||
* http://msdn.microsoft.com/en-us/library/ms526356%28v=exchg.10%29.aspx | |||
* | |||
* TODO Deprecate the public Chunks in favour of Property Lookups | |||
*/ | |||
public final class Chunks implements ChunkGroupWithProperties { | |||
private static POILogger logger = POILogFactory.getLogger(Chunks.class); | |||
/** | |||
* Holds all the chunks that were found, indexed by their MAPIProperty. | |||
* Normally a property will have zero chunks (fixed sized) or one chunk | |||
* (variable size), but in some cases (eg Unknown) you may get more. | |||
*/ | |||
private Map<MAPIProperty,List<Chunk>> allChunks = new HashMap<MAPIProperty,List<Chunk>>(); | |||
/** Type of message that the MSG represents (ie. IPM.Note) */ | |||
public StringChunk messageClass; | |||
/** BODY Chunk, for plain/text messages */ | |||
public StringChunk textBodyChunk; | |||
/** BODY Html Chunk, for html messages */ | |||
public StringChunk htmlBodyChunkString; | |||
public ByteChunk htmlBodyChunkBinary; | |||
/** BODY Rtf Chunk, for Rtf (Rich) messages */ | |||
public ByteChunk rtfBodyChunk; | |||
/** Subject link chunk, in plain/text */ | |||
public StringChunk subjectChunk; | |||
/** | |||
* Value that is in the TO field (not actually the addresses as they are | |||
* stored in recip directory nodes | |||
*/ | |||
public StringChunk displayToChunk; | |||
/** Value that is in the FROM field */ | |||
public StringChunk displayFromChunk; | |||
/** value that shows in the CC field */ | |||
public StringChunk displayCCChunk; | |||
/** Value that shows in the BCC field */ | |||
public StringChunk displayBCCChunk; | |||
/** Sort of like the subject line, but without the RE: and FWD: parts. */ | |||
public StringChunk conversationTopic; | |||
/** Type of server that the message originated from (SMTP, etc). */ | |||
public StringChunk sentByServerType; | |||
/** The email headers */ | |||
public StringChunk messageHeaders; | |||
/** TODO */ | |||
public MessageSubmissionChunk submissionChunk; | |||
/** TODO */ | |||
public StringChunk emailFromChunk; | |||
/** The message ID */ | |||
public StringChunk messageId; | |||
/** The message properties */ | |||
private MessagePropertiesChunk messageProperties; | |||
public Map<MAPIProperty,List<PropertyValue>> getProperties() { | |||
if (messageProperties != null) { | |||
return messageProperties.getProperties(); | |||
} | |||
else return Collections.emptyMap(); | |||
} | |||
public Map<MAPIProperty, PropertyValue> getRawProperties() { | |||
if (messageProperties != null) { | |||
return messageProperties.getRawProperties(); | |||
} | |||
else return Collections.emptyMap(); | |||
} | |||
public Map<MAPIProperty,List<Chunk>> getAll() { | |||
return allChunks; | |||
} | |||
public Chunk[] getChunks() { | |||
ArrayList<Chunk> chunks = new ArrayList<Chunk>(allChunks.size()); | |||
for (List<Chunk> c : allChunks.values()) { | |||
chunks.addAll(c); | |||
} | |||
return chunks.toArray(new Chunk[chunks.size()]); | |||
} | |||
/** | |||
* Called by the parser whenever a chunk is found. | |||
*/ | |||
public void record(Chunk chunk) { | |||
// Work out what MAPIProperty this corresponds to | |||
MAPIProperty prop = MAPIProperty.get(chunk.getChunkId()); | |||
// Assign it for easy lookup, as best we can | |||
if(prop == MAPIProperty.MESSAGE_CLASS) { | |||
messageClass = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.INTERNET_MESSAGE_ID) { | |||
messageId = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.MESSAGE_SUBMISSION_ID) { | |||
// TODO - parse | |||
submissionChunk = (MessageSubmissionChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.RECEIVED_BY_ADDRTYPE) { | |||
sentByServerType = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.TRANSPORT_MESSAGE_HEADERS) { | |||
messageHeaders = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.CONVERSATION_TOPIC) { | |||
conversationTopic = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.SUBJECT) { | |||
subjectChunk = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.ORIGINAL_SUBJECT) { | |||
// TODO | |||
} | |||
else if(prop == MAPIProperty.DISPLAY_TO) { | |||
displayToChunk = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.DISPLAY_CC) { | |||
displayCCChunk = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.DISPLAY_BCC) { | |||
displayBCCChunk = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.SENDER_EMAIL_ADDRESS) { | |||
emailFromChunk = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.SENDER_NAME) { | |||
displayFromChunk = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.BODY) { | |||
textBodyChunk = (StringChunk)chunk; | |||
} | |||
else if(prop == MAPIProperty.BODY_HTML) { | |||
if(chunk instanceof StringChunk) { | |||
htmlBodyChunkString = (StringChunk)chunk; | |||
} | |||
if(chunk instanceof ByteChunk) { | |||
htmlBodyChunkBinary = (ByteChunk)chunk; | |||
} | |||
} | |||
else if(prop == MAPIProperty.RTF_COMPRESSED) { | |||
rtfBodyChunk = (ByteChunk)chunk; | |||
} | |||
else if(chunk instanceof MessagePropertiesChunk) { | |||
messageProperties = (MessagePropertiesChunk) chunk; | |||
} | |||
// And add to the main list | |||
if (allChunks.get(prop) == null) { | |||
allChunks.put(prop, new ArrayList<Chunk>()); | |||
} | |||
allChunks.get(prop).add(chunk); | |||
} | |||
public void chunksComplete() { | |||
if (messageProperties != null) { | |||
messageProperties.matchVariableSizedPropertiesToChunks(); | |||
} else { | |||
logger.log(POILogger.WARN, "Message didn't contain a root list of properties!"); | |||
} | |||
} | |||
private static POILogger logger = POILogFactory.getLogger(Chunks.class); | |||
/** | |||
* Holds all the chunks that were found, indexed by their MAPIProperty. | |||
* Normally a property will have zero chunks (fixed sized) or one chunk | |||
* (variable size), but in some cases (eg Unknown) you may get more. | |||
*/ | |||
private Map<MAPIProperty, List<Chunk>> allChunks = new HashMap<MAPIProperty, List<Chunk>>(); | |||
/** Type of message that the MSG represents (ie. IPM.Note) */ | |||
public StringChunk messageClass; | |||
/** BODY Chunk, for plain/text messages */ | |||
public StringChunk textBodyChunk; | |||
/** BODY Html Chunk, for html messages */ | |||
public StringChunk htmlBodyChunkString; | |||
public ByteChunk htmlBodyChunkBinary; | |||
/** BODY Rtf Chunk, for Rtf (Rich) messages */ | |||
public ByteChunk rtfBodyChunk; | |||
/** Subject link chunk, in plain/text */ | |||
public StringChunk subjectChunk; | |||
/** | |||
* Value that is in the TO field (not actually the addresses as they are | |||
* stored in recip directory nodes | |||
*/ | |||
public StringChunk displayToChunk; | |||
/** Value that is in the FROM field */ | |||
public StringChunk displayFromChunk; | |||
/** value that shows in the CC field */ | |||
public StringChunk displayCCChunk; | |||
/** Value that shows in the BCC field */ | |||
public StringChunk displayBCCChunk; | |||
/** Sort of like the subject line, but without the RE: and FWD: parts. */ | |||
public StringChunk conversationTopic; | |||
/** Type of server that the message originated from (SMTP, etc). */ | |||
public StringChunk sentByServerType; | |||
/** The email headers */ | |||
public StringChunk messageHeaders; | |||
/** TODO */ | |||
public MessageSubmissionChunk submissionChunk; | |||
/** TODO */ | |||
public StringChunk emailFromChunk; | |||
/** The message ID */ | |||
public StringChunk messageId; | |||
/** The message properties */ | |||
private MessagePropertiesChunk messageProperties; | |||
public Map<MAPIProperty, List<PropertyValue>> getProperties() { | |||
if (messageProperties != null) { | |||
return messageProperties.getProperties(); | |||
} else | |||
return Collections.emptyMap(); | |||
} | |||
public Map<MAPIProperty, PropertyValue> getRawProperties() { | |||
if (messageProperties != null) { | |||
return messageProperties.getRawProperties(); | |||
} else | |||
return Collections.emptyMap(); | |||
} | |||
public Map<MAPIProperty, List<Chunk>> getAll() { | |||
return allChunks; | |||
} | |||
public Chunk[] getChunks() { | |||
ArrayList<Chunk> chunks = new ArrayList<Chunk>(allChunks.size()); | |||
for (List<Chunk> c : allChunks.values()) { | |||
chunks.addAll(c); | |||
} | |||
return chunks.toArray(new Chunk[chunks.size()]); | |||
} | |||
/** | |||
* Called by the parser whenever a chunk is found. | |||
*/ | |||
public void record(Chunk chunk) { | |||
// Work out what MAPIProperty this corresponds to | |||
MAPIProperty prop = MAPIProperty.get(chunk.getChunkId()); | |||
// Assign it for easy lookup, as best we can | |||
if (prop == MAPIProperty.MESSAGE_CLASS) { | |||
messageClass = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.INTERNET_MESSAGE_ID) { | |||
messageId = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.MESSAGE_SUBMISSION_ID) { | |||
// TODO - parse | |||
submissionChunk = (MessageSubmissionChunk) chunk; | |||
} else if (prop == MAPIProperty.RECEIVED_BY_ADDRTYPE) { | |||
sentByServerType = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.TRANSPORT_MESSAGE_HEADERS) { | |||
messageHeaders = (StringChunk) chunk; | |||
} | |||
else if (prop == MAPIProperty.CONVERSATION_TOPIC) { | |||
conversationTopic = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.SUBJECT) { | |||
subjectChunk = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.ORIGINAL_SUBJECT) { | |||
// TODO | |||
} | |||
else if (prop == MAPIProperty.DISPLAY_TO) { | |||
displayToChunk = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.DISPLAY_CC) { | |||
displayCCChunk = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.DISPLAY_BCC) { | |||
displayBCCChunk = (StringChunk) chunk; | |||
} | |||
else if (prop == MAPIProperty.SENDER_EMAIL_ADDRESS) { | |||
emailFromChunk = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.SENDER_NAME) { | |||
displayFromChunk = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.BODY) { | |||
textBodyChunk = (StringChunk) chunk; | |||
} else if (prop == MAPIProperty.BODY_HTML) { | |||
if (chunk instanceof StringChunk) { | |||
htmlBodyChunkString = (StringChunk) chunk; | |||
} | |||
if (chunk instanceof ByteChunk) { | |||
htmlBodyChunkBinary = (ByteChunk) chunk; | |||
} | |||
} else if (prop == MAPIProperty.RTF_COMPRESSED) { | |||
rtfBodyChunk = (ByteChunk) chunk; | |||
} else if (chunk instanceof MessagePropertiesChunk) { | |||
messageProperties = (MessagePropertiesChunk) chunk; | |||
} | |||
// And add to the main list | |||
if (allChunks.get(prop) == null) { | |||
allChunks.put(prop, new ArrayList<Chunk>()); | |||
} | |||
allChunks.get(prop).add(chunk); | |||
} | |||
public void chunksComplete() { | |||
if (messageProperties != null) { | |||
messageProperties.matchVariableSizedPropertiesToChunks(); | |||
} else { | |||
logger.log(POILogger.WARN, | |||
"Message didn't contain a root list of properties!"); | |||
} | |||
} | |||
} |
@@ -25,33 +25,28 @@ import org.apache.poi.hsmf.datatypes.Types.MAPIType; | |||
import org.apache.poi.poifs.filesystem.DirectoryNode; | |||
/** | |||
* A Chunk that is just a placeholder in the | |||
* MAPIMessage directory structure, which | |||
* contains children. | |||
* This is most commonly used with nested | |||
* MAPIMessages | |||
* A Chunk that is just a placeholder in the MAPIMessage directory structure, | |||
* which contains children. This is most commonly used with nested MAPIMessages | |||
*/ | |||
public class DirectoryChunk extends Chunk { | |||
private DirectoryNode dir; | |||
public DirectoryChunk(DirectoryNode dir, String namePrefix, int chunkId, MAPIType type) { | |||
super(namePrefix, chunkId, type); | |||
this.dir = dir; | |||
} | |||
/** | |||
* Returns the directory entry for this chunk. | |||
* You can then use standard POIFS methods to | |||
* enumerate the entries in it. | |||
* Returns the directory entry for this chunk. You can then use standard | |||
* POIFS methods to enumerate the entries in it. | |||
*/ | |||
public DirectoryNode getDirectory() { | |||
return dir; | |||
} | |||
/** | |||
* Treats the directory as an embeded MAPIMessage | |||
* (it normally is one), and returns a MAPIMessage | |||
* object to process it with. | |||
* Treats the directory as an embeded MAPIMessage (it normally is one), and | |||
* returns a MAPIMessage object to process it with. | |||
*/ | |||
public MAPIMessage getAsEmbededMessage() throws IOException { | |||
return new MAPIMessage(dir); |
@@ -24,66 +24,68 @@ import java.io.OutputStream; | |||
import org.apache.poi.util.LittleEndian; | |||
/** | |||
* A {@link PropertiesChunk} for a Message or Embedded-Message. | |||
* This has a 32 byte header | |||
* A {@link PropertiesChunk} for a Message or Embedded-Message. This has a 32 | |||
* byte header | |||
*/ | |||
public class MessagePropertiesChunk extends PropertiesChunk { | |||
private long nextRecipientId; | |||
private long nextAttachmentId; | |||
private long recipientCount; | |||
private long attachmentCount; | |||
public MessagePropertiesChunk(ChunkGroup parentGroup) { | |||
super(parentGroup); | |||
} | |||
public long getNextRecipientId() { | |||
return nextRecipientId; | |||
} | |||
public long getNextAttachmentId() { | |||
return nextAttachmentId; | |||
} | |||
public long getRecipientCount() { | |||
return recipientCount; | |||
} | |||
public long getAttachmentCount() { | |||
return attachmentCount; | |||
} | |||
@Override | |||
public void readValue(InputStream stream) throws IOException { | |||
// 8 bytes of reserved zeros | |||
LittleEndian.readLong(stream); | |||
// Nexts and counts | |||
nextRecipientId = LittleEndian.readUInt(stream); | |||
nextAttachmentId = LittleEndian.readUInt(stream); | |||
recipientCount = LittleEndian.readUInt(stream); | |||
attachmentCount = LittleEndian.readUInt(stream); | |||
// 8 bytes of reserved zeros | |||
LittleEndian.readLong(stream); | |||
// Now properties | |||
readProperties(stream); | |||
} | |||
@Override | |||
public void writeValue(OutputStream out) throws IOException { | |||
// 8 bytes of reserved zeros | |||
out.write(new byte[8]); | |||
// Nexts and counts | |||
LittleEndian.putUInt(nextRecipientId, out); | |||
LittleEndian.putUInt(nextAttachmentId, out); | |||
LittleEndian.putUInt(recipientCount, out); | |||
LittleEndian.putUInt(attachmentCount, out); | |||
// 8 bytes of reserved zeros | |||
out.write(new byte[8]); | |||
// Now properties | |||
writeProperties(out); | |||
} | |||
private long nextRecipientId; | |||
private long nextAttachmentId; | |||
private long recipientCount; | |||
private long attachmentCount; | |||
public MessagePropertiesChunk(ChunkGroup parentGroup) { | |||
super(parentGroup); | |||
} | |||
public long getNextRecipientId() { | |||
return nextRecipientId; | |||
} | |||
public long getNextAttachmentId() { | |||
return nextAttachmentId; | |||
} | |||
public long getRecipientCount() { | |||
return recipientCount; | |||
} | |||
public long getAttachmentCount() { | |||
return attachmentCount; | |||
} | |||
@Override | |||
public void readValue(InputStream stream) throws IOException { | |||
// 8 bytes of reserved zeros | |||
LittleEndian.readLong(stream); | |||
// Nexts and counts | |||
nextRecipientId = LittleEndian.readUInt(stream); | |||
nextAttachmentId = LittleEndian.readUInt(stream); | |||
recipientCount = LittleEndian.readUInt(stream); | |||
attachmentCount = LittleEndian.readUInt(stream); | |||
// 8 bytes of reserved zeros | |||
LittleEndian.readLong(stream); | |||
// Now properties | |||
readProperties(stream); | |||
} | |||
@Override | |||
public void writeValue(OutputStream out) throws IOException { | |||
// 8 bytes of reserved zeros | |||
out.write(new byte[8]); | |||
// Nexts and counts | |||
LittleEndian.putUInt(nextRecipientId, out); | |||
LittleEndian.putUInt(nextAttachmentId, out); | |||
LittleEndian.putUInt(recipientCount, out); | |||
LittleEndian.putUInt(attachmentCount, out); | |||
// 8 bytes of reserved zeros | |||
out.write(new byte[8]); | |||
// Now properties | |||
writeProperties(out); | |||
} | |||
} |
@@ -31,100 +31,106 @@ import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
/** | |||
* A Chunk that holds the details given back by the | |||
* server at submission time. | |||
* This includes the date the message was given to the | |||
* server, and an ID that's used if you want to cancel | |||
* a message or similar | |||
* A Chunk that holds the details given back by the server at submission time. | |||
* This includes the date the message was given to the server, and an ID that's | |||
* used if you want to cancel a message or similar | |||
*/ | |||
public class MessageSubmissionChunk extends Chunk { | |||
private static POILogger logger = POILogFactory.getLogger(MessageSubmissionChunk.class); | |||
private String rawId; | |||
private Calendar date; | |||
private static final Pattern datePatern = | |||
Pattern.compile("(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)Z?"); | |||
/** | |||
* Creates a Byte Chunk. | |||
*/ | |||
public MessageSubmissionChunk(String namePrefix, int chunkId, MAPIType type) { | |||
super(namePrefix, chunkId, type); | |||
} | |||
/** | |||
* Create a Byte Chunk, with the specified | |||
* type. | |||
*/ | |||
public MessageSubmissionChunk(int chunkId, MAPIType type) { | |||
super(chunkId, type); | |||
} | |||
public void readValue(InputStream value) throws IOException { | |||
// Stored in the file as us-ascii | |||
byte[] data = IOUtils.toByteArray(value); | |||
rawId = new String(data, Charset.forName("ASCII")); | |||
// Now process the date | |||
String[] parts = rawId.split(";"); | |||
for(String part : parts) { | |||
if(part.startsWith("l=")) { | |||
// Format of this bit appears to be l=<id>-<time>-<number> | |||
// ID may contain hyphens. | |||
String dateS = null; | |||
final int numberPartBegin = part.lastIndexOf('-'); | |||
if (numberPartBegin != -1) { | |||
final int datePartBegin = part.lastIndexOf('-', numberPartBegin-1); | |||
if (datePartBegin != -1 && | |||
// cannot extract date if only one hyphen is in the string... | |||
numberPartBegin > datePartBegin) { | |||
dateS = part.substring(datePartBegin + 1, numberPartBegin); | |||
private static POILogger logger = POILogFactory | |||
.getLogger(MessageSubmissionChunk.class); | |||
private String rawId; | |||
private Calendar date; | |||
private static final Pattern datePatern = Pattern | |||
.compile("(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)Z?"); | |||
/** | |||
* Creates a Byte Chunk. | |||
*/ | |||
public MessageSubmissionChunk(String namePrefix, int chunkId, | |||
MAPIType type) { | |||
super(namePrefix, chunkId, type); | |||
} | |||
/** | |||
* Create a Byte Chunk, with the specified type. | |||
*/ | |||
public MessageSubmissionChunk(int chunkId, MAPIType type) { | |||
super(chunkId, type); | |||
} | |||
public void readValue(InputStream value) throws IOException { | |||
// Stored in the file as us-ascii | |||
byte[] data = IOUtils.toByteArray(value); | |||
rawId = new String(data, Charset.forName("ASCII")); | |||
// Now process the date | |||
String[] parts = rawId.split(";"); | |||
for (String part : parts) { | |||
if (part.startsWith("l=")) { | |||
// Format of this bit appears to be l=<id>-<time>-<number> | |||
// ID may contain hyphens. | |||
String dateS = null; | |||
final int numberPartBegin = part.lastIndexOf('-'); | |||
if (numberPartBegin != -1) { | |||
final int datePartBegin = part.lastIndexOf('-', | |||
numberPartBegin - 1); | |||
if (datePartBegin != -1 && | |||
// cannot extract date if only one hyphen is in the | |||
// string... | |||
numberPartBegin > datePartBegin) { | |||
dateS = part.substring(datePartBegin + 1, | |||
numberPartBegin); | |||
} | |||
} | |||
if (dateS != null) { | |||
// Should be yymmddhhmmssZ | |||
Matcher m = datePatern.matcher(dateS); | |||
if (m.matches()) { | |||
date = LocaleUtil.getLocaleCalendar(); | |||
// work around issues with dates like 1989, which appear as "89" here | |||
int year = Integer.parseInt(m.group(1)); | |||
date.set(Calendar.YEAR, year + (year > 80 ? 1900 : 2000)); | |||
// Java is 0 based | |||
date.set(Calendar.MONTH, Integer.parseInt(m.group(2)) - 1); | |||
date.set(Calendar.DATE, Integer.parseInt(m.group(3))); | |||
date.set(Calendar.HOUR_OF_DAY, | |||
Integer.parseInt(m.group(4))); | |||
date.set(Calendar.MINUTE, Integer.parseInt(m.group(5))); | |||
date.set(Calendar.SECOND, Integer.parseInt(m.group(6))); | |||
date.clear(Calendar.MILLISECOND); | |||
} else { | |||
logger.log(POILogger.WARN, | |||
"Warning - unable to make sense of date " | |||
+ dateS); | |||
} | |||
} | |||
} | |||
if (dateS != null) { | |||
// Should be yymmddhhmmssZ | |||
Matcher m = datePatern.matcher(dateS); | |||
if(m.matches()) { | |||
date = LocaleUtil.getLocaleCalendar(); | |||
// work around issues with dates like 1989, which appear as "89" here | |||
int year = Integer.parseInt(m.group(1)); | |||
date.set(Calendar.YEAR, year + (year > 80 ? 1900 : 2000)); | |||
date.set(Calendar.MONTH, Integer.parseInt(m.group(2)) - 1); // Java is 0 based | |||
date.set(Calendar.DATE, Integer.parseInt(m.group(3))); | |||
date.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(4))); | |||
date.set(Calendar.MINUTE, Integer.parseInt(m.group(5))); | |||
date.set(Calendar.SECOND, Integer.parseInt(m.group(6))); | |||
date.clear(Calendar.MILLISECOND); | |||
} else { | |||
logger.log(POILogger.WARN, "Warning - unable to make sense of date " + dateS); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
public void writeValue(OutputStream out) throws IOException { | |||
byte[] data = rawId.getBytes(Charset.forName("ASCII")); | |||
out.write(data); | |||
} | |||
/** | |||
* @return the date that the server accepted the | |||
* message, as found from the message ID it generated. | |||
* | |||
*/ | |||
public Calendar getAcceptedAtTime() { | |||
return date; | |||
} | |||
/** | |||
* @return the full ID that the server generated when | |||
* it accepted the message. | |||
*/ | |||
public String getSubmissionId() { | |||
return rawId; | |||
} | |||
} | |||
} | |||
public void writeValue(OutputStream out) throws IOException { | |||
byte[] data = rawId.getBytes(Charset.forName("ASCII")); | |||
out.write(data); | |||
} | |||
/** | |||
* @return the date that the server accepted the message, as found from the | |||
* message ID it generated. | |||
* | |||
*/ | |||
public Calendar getAcceptedAtTime() { | |||
return date; | |||
} | |||
/** | |||
* @return the full ID that the server generated when it accepted the | |||
* message. | |||
*/ | |||
public String getSubmissionId() { | |||
return rawId; | |||
} | |||
} |
@@ -20,37 +20,35 @@ package org.apache.poi.hsmf.datatypes; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
/** | |||
* Collection of convenience chunks for the | |||
* NameID part of an outlook file | |||
* Collection of convenience chunks for the NameID part of an outlook file | |||
*/ | |||
public final class NameIdChunks implements ChunkGroup { | |||
public static final String NAME = "__nameid_version1.0"; | |||
/** Holds all the chunks that were found. */ | |||
private List<Chunk> allChunks = new ArrayList<Chunk>(); | |||
public Chunk[] getAll() { | |||
return allChunks.toArray(new Chunk[allChunks.size()]); | |||
} | |||
public Chunk[] getChunks() { | |||
return getAll(); | |||
} | |||
/** | |||
* Called by the parser whenever a chunk is found. | |||
*/ | |||
public void record(Chunk chunk) { | |||
allChunks.add(chunk); | |||
} | |||
/** | |||
* Used to flag that all the chunks of the NameID | |||
* have now been located. | |||
*/ | |||
public void chunksComplete() { | |||
// Currently, we don't need to do anything special once | |||
// all the chunks have been located | |||
} | |||
public static final String NAME = "__nameid_version1.0"; | |||
/** Holds all the chunks that were found. */ | |||
private List<Chunk> allChunks = new ArrayList<Chunk>(); | |||
public Chunk[] getAll() { | |||
return allChunks.toArray(new Chunk[allChunks.size()]); | |||
} | |||
public Chunk[] getChunks() { | |||
return getAll(); | |||
} | |||
/** | |||
* Called by the parser whenever a chunk is found. | |||
*/ | |||
public void record(Chunk chunk) { | |||
allChunks.add(chunk); | |||
} | |||
/** | |||
* Used to flag that all the chunks of the NameID have now been located. | |||
*/ | |||
public void chunksComplete() { | |||
// Currently, we don't need to do anything special once | |||
// all the chunks have been located | |||
} | |||
} |
@@ -42,240 +42,238 @@ import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
/** | |||
* <p>A Chunk which holds (single) fixed-length properties, and pointer | |||
* to the variable length ones / multi-valued ones (which get their | |||
* own chunk). | |||
* <p>There are two kinds of PropertiesChunks, which differ only in | |||
* their headers. | |||
* <p> | |||
* A Chunk which holds (single) fixed-length properties, and pointer to the | |||
* variable length ones / multi-valued ones (which get their own chunk). | |||
* <p> | |||
* There are two kinds of PropertiesChunks, which differ only in their headers. | |||
*/ | |||
public abstract class PropertiesChunk extends Chunk { | |||
public static final String NAME = "__properties_version1.0"; | |||
/** For logging problems we spot with the file */ | |||
private POILogger logger = POILogFactory.getLogger(PropertiesChunk.class); | |||
public static final String NAME = "__properties_version1.0"; | |||
/** | |||
* Holds properties, indexed by type. If a property is multi-valued, | |||
* or variable length, it will be held via a {@link ChunkBasedPropertyValue}. | |||
*/ | |||
private Map<MAPIProperty, PropertyValue> properties = | |||
new HashMap<MAPIProperty, PropertyValue>(); | |||
/** For logging problems we spot with the file */ | |||
private POILogger logger = POILogFactory.getLogger(PropertiesChunk.class); | |||
/** | |||
* The ChunkGroup that these properties apply to. Used when | |||
* matching chunks to variable sized and multi-valued properties | |||
*/ | |||
private ChunkGroup parentGroup; | |||
/** | |||
* Creates a Properties Chunk. | |||
*/ | |||
protected PropertiesChunk(ChunkGroup parentGroup) { | |||
super(NAME, -1, Types.UNKNOWN); | |||
this.parentGroup = parentGroup; | |||
} | |||
/** | |||
* Holds properties, indexed by type. If a property is multi-valued, or | |||
* variable length, it will be held via a {@link ChunkBasedPropertyValue}. | |||
*/ | |||
private Map<MAPIProperty, PropertyValue> properties = new HashMap<MAPIProperty, PropertyValue>(); | |||
@Override | |||
public String getEntryName() { | |||
return NAME; | |||
} | |||
/** | |||
* Returns all the properties in the chunk, without | |||
* looking up any chunk-based values | |||
*/ | |||
public Map<MAPIProperty, PropertyValue> getRawProperties() { | |||
return properties; | |||
} | |||
/** | |||
* The ChunkGroup that these properties apply to. Used when matching chunks | |||
* to variable sized and multi-valued properties | |||
*/ | |||
private ChunkGroup parentGroup; | |||
/** | |||
* <p>Returns all the properties in the chunk, along with their | |||
* values. | |||
* <p>Any chunk-based values will be looked up and returned as such | |||
*/ | |||
public Map<MAPIProperty, List<PropertyValue>> getProperties() { | |||
Map<MAPIProperty, List<PropertyValue>> props = | |||
new HashMap<MAPIProperty, List<PropertyValue>>(properties.size()); | |||
for (MAPIProperty prop : properties.keySet()) { | |||
props.put(prop, getValues(prop)); | |||
} | |||
return props; | |||
} | |||
/** | |||
* Creates a Properties Chunk. | |||
*/ | |||
protected PropertiesChunk(ChunkGroup parentGroup) { | |||
super(NAME, -1, Types.UNKNOWN); | |||
this.parentGroup = parentGroup; | |||
} | |||
/** | |||
* Returns all values for the given property, looking up chunk based | |||
* ones as required, of null if none exist | |||
*/ | |||
public List<PropertyValue> getValues(MAPIProperty property) { | |||
PropertyValue val = properties.get(property); | |||
if (val == null) { | |||
return null; | |||
} | |||
if (val instanceof ChunkBasedPropertyValue) { | |||
// ChunkBasedPropertyValue cval = (ChunkBasedPropertyValue)val; | |||
// TODO Lookup | |||
return Collections.emptyList(); | |||
} else { | |||
return Collections.singletonList(val); | |||
} | |||
} | |||
@Override | |||
public String getEntryName() { | |||
return NAME; | |||
} | |||
/** | |||
* Returns the value / pointer to the value chunk of | |||
* the property, or null if none exists | |||
*/ | |||
public PropertyValue getRawValue(MAPIProperty property) { | |||
return properties.get(property); | |||
} | |||
/** | |||
* Called once the parent ChunkGroup has been populated, to match | |||
* up the Chunks in it with our Variable Sized Properties. | |||
*/ | |||
protected void matchVariableSizedPropertiesToChunks() { | |||
// Index the Parent Group chunks for easy lookup | |||
// TODO Is this the right way? | |||
Map<Integer,Chunk> chunks = new HashMap<Integer, Chunk>(); | |||
for (Chunk chunk : parentGroup.getChunks()) { | |||
chunks.put(chunk.chunkId, chunk); | |||
} | |||
// Loop over our values, looking for chunk based ones | |||
for (PropertyValue val : properties.values()) { | |||
if (val instanceof ChunkBasedPropertyValue) { | |||
ChunkBasedPropertyValue cVal = (ChunkBasedPropertyValue)val; | |||
Chunk chunk = chunks.get(cVal.getProperty().id); | |||
//System.err.println(cVal.getProperty() + " = " + cVal + " -> " + HexDump.toHex(cVal.data)); | |||
// TODO Make sense of the raw offset value | |||
if (chunk != null) { | |||
cVal.setValue(chunk); | |||
} else { | |||
logger.log(POILogger.WARN, "No chunk found matching Property " + cVal); | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Returns all the properties in the chunk, without looking up any | |||
* chunk-based values | |||
*/ | |||
public Map<MAPIProperty, PropertyValue> getRawProperties() { | |||
return properties; | |||
} | |||
protected void readProperties(InputStream value) throws IOException { | |||
boolean going = true; | |||
while (going) { | |||
try { | |||
// Read in the header | |||
int typeID = LittleEndian.readUShort(value); | |||
int id = LittleEndian.readUShort(value); | |||
long flags = LittleEndian.readUInt(value); | |||
// Turn the Type and ID into helper objects | |||
MAPIType type = Types.getById(typeID); | |||
MAPIProperty prop = MAPIProperty.get(id); | |||
// Wrap properties we don't know about as custom ones | |||
if (prop == MAPIProperty.UNKNOWN) { | |||
prop = MAPIProperty.createCustom(id, type, "Unknown " + id); | |||
} | |||
if (type == null) { | |||
logger.log(POILogger.WARN, "Invalid type found, expected ", prop.usualType, | |||
" but got ", typeID, " for property ", prop); | |||
going = false; | |||
break; | |||
} | |||
// Sanity check the property's type against the value's type | |||
if (prop.usualType != type) { | |||
// Is it an allowed substitution? | |||
if (type == Types.ASCII_STRING && prop.usualType == Types.UNICODE_STRING || | |||
type == Types.UNICODE_STRING && prop.usualType == Types.ASCII_STRING) { | |||
// It's fine to go with the specified instead of the normal | |||
} else if (prop.usualType == Types.UNKNOWN) { | |||
// We don't know what this property normally is, but it has come | |||
// through with a valid type, so use that | |||
logger.log(POILogger.INFO, "Property definition for ", prop, | |||
" is missing a type definition, found a value with type ", type); | |||
/** | |||
* <p> | |||
* Returns all the properties in the chunk, along with their values. | |||
* <p> | |||
* Any chunk-based values will be looked up and returned as such | |||
*/ | |||
public Map<MAPIProperty, List<PropertyValue>> getProperties() { | |||
Map<MAPIProperty, List<PropertyValue>> props = | |||
new HashMap<MAPIProperty, List<PropertyValue>>(properties.size()); | |||
for (MAPIProperty prop : properties.keySet()) { | |||
props.put(prop, getValues(prop)); | |||
} | |||
return props; | |||
} | |||
/** | |||
* Returns all values for the given property, looking up chunk based ones as | |||
* required, of null if none exist | |||
*/ | |||
public List<PropertyValue> getValues(MAPIProperty property) { | |||
PropertyValue val = properties.get(property); | |||
if (val == null) { | |||
return null; | |||
} | |||
if (val instanceof ChunkBasedPropertyValue) { | |||
// ChunkBasedPropertyValue cval = (ChunkBasedPropertyValue)val; | |||
// TODO Lookup | |||
return Collections.emptyList(); | |||
} else { | |||
return Collections.singletonList(val); | |||
} | |||
} | |||
/** | |||
* Returns the value / pointer to the value chunk of the property, or null | |||
* if none exists | |||
*/ | |||
public PropertyValue getRawValue(MAPIProperty property) { | |||
return properties.get(property); | |||
} | |||
/** | |||
* Called once the parent ChunkGroup has been populated, to match up the | |||
* Chunks in it with our Variable Sized Properties. | |||
*/ | |||
protected void matchVariableSizedPropertiesToChunks() { | |||
// Index the Parent Group chunks for easy lookup | |||
// TODO Is this the right way? | |||
Map<Integer, Chunk> chunks = new HashMap<Integer, Chunk>(); | |||
for (Chunk chunk : parentGroup.getChunks()) { | |||
chunks.put(chunk.chunkId, chunk); | |||
} | |||
// Loop over our values, looking for chunk based ones | |||
for (PropertyValue val : properties.values()) { | |||
if (val instanceof ChunkBasedPropertyValue) { | |||
ChunkBasedPropertyValue cVal = (ChunkBasedPropertyValue) val; | |||
Chunk chunk = chunks.get(cVal.getProperty().id); | |||
// System.err.println(cVal.getProperty() + " = " + cVal + " -> " | |||
// + HexDump.toHex(cVal.data)); | |||
// TODO Make sense of the raw offset value | |||
if (chunk != null) { | |||
cVal.setValue(chunk); | |||
} else { | |||
// Oh dear, something has gone wrong... | |||
logger.log(POILogger.WARN, "Type mismatch, expected ", prop.usualType, | |||
" but got ", type, " for property ", prop); | |||
going = false; | |||
break; | |||
logger.log(POILogger.WARN, "No chunk found matching Property " + cVal); | |||
} | |||
} | |||
// TODO Detect if it is multi-valued, since if it is | |||
// then even fixed-length strings store their multiple | |||
// values in another chunk (much as variable length ones) | |||
// Work out how long the "data" is | |||
// This might be the actual data, or just a pointer | |||
// to another chunk which holds the data itself | |||
boolean isPointer = false; | |||
int length = type.getLength(); | |||
if (! type.isFixedLength()) { | |||
isPointer = true; | |||
length = 8; | |||
} | |||
// Grab the data block | |||
byte[] data = new byte[length]; | |||
IOUtils.readFully(value, data); | |||
// Skip over any padding | |||
if (length < 8) { | |||
byte[] padding = new byte[8-length]; | |||
IOUtils.readFully(value, padding); | |||
} | |||
// Wrap and store | |||
PropertyValue propVal = null; | |||
if (isPointer) { | |||
// We'll match up the chunk later | |||
propVal = new ChunkBasedPropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.NULL) { | |||
propVal = new NullPropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.BOOLEAN) { | |||
propVal = new BooleanPropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.SHORT) { | |||
propVal = new ShortPropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.LONG) { | |||
propVal = new LongPropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.LONG_LONG) { | |||
propVal = new LongLongPropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.FLOAT) { | |||
propVal = new FloatPropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.DOUBLE) { | |||
propVal = new DoublePropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.CURRENCY) { | |||
propVal = new CurrencyPropertyValue(prop, flags, data); | |||
} | |||
else if (type == Types.TIME) { | |||
propVal = new TimePropertyValue(prop, flags, data); | |||
} | |||
// TODO Add in the rest of the types | |||
else { | |||
propVal = new PropertyValue(prop, flags, data); | |||
} | |||
} | |||
} | |||
protected void readProperties(InputStream value) throws IOException { | |||
boolean going = true; | |||
while (going) { | |||
try { | |||
// Read in the header | |||
int typeID = LittleEndian.readUShort(value); | |||
int id = LittleEndian.readUShort(value); | |||
long flags = LittleEndian.readUInt(value); | |||
// Turn the Type and ID into helper objects | |||
MAPIType type = Types.getById(typeID); | |||
MAPIProperty prop = MAPIProperty.get(id); | |||
// Wrap properties we don't know about as custom ones | |||
if (prop == MAPIProperty.UNKNOWN) { | |||
prop = MAPIProperty.createCustom(id, type, "Unknown " + id); | |||
} | |||
if (type == null) { | |||
logger.log(POILogger.WARN, "Invalid type found, expected ", | |||
prop.usualType, " but got ", typeID, | |||
" for property ", prop); | |||
going = false; | |||
break; | |||
} | |||
// Sanity check the property's type against the value's type | |||
if (prop.usualType != type) { | |||
// Is it an allowed substitution? | |||
if (type == Types.ASCII_STRING | |||
&& prop.usualType == Types.UNICODE_STRING | |||
|| type == Types.UNICODE_STRING | |||
&& prop.usualType == Types.ASCII_STRING) { | |||
// It's fine to go with the specified instead of the | |||
// normal | |||
} else if (prop.usualType == Types.UNKNOWN) { | |||
// We don't know what this property normally is, but it | |||
// has come | |||
// through with a valid type, so use that | |||
logger.log(POILogger.INFO, "Property definition for ", prop, | |||
" is missing a type definition, found a value with type ", type); | |||
} else { | |||
// Oh dear, something has gone wrong... | |||
logger.log(POILogger.WARN, "Type mismatch, expected ", | |||
prop.usualType, " but got ", type, " for property ", prop); | |||
going = false; | |||
break; | |||
} | |||
} | |||
// TODO Detect if it is multi-valued, since if it is | |||
// then even fixed-length strings store their multiple | |||
// values in another chunk (much as variable length ones) | |||
if (properties.get(prop) != null) { | |||
logger.log(POILogger.WARN, "Duplicate values found for " + prop); | |||
// Work out how long the "data" is | |||
// This might be the actual data, or just a pointer | |||
// to another chunk which holds the data itself | |||
boolean isPointer = false; | |||
int length = type.getLength(); | |||
if (!type.isFixedLength()) { | |||
isPointer = true; | |||
length = 8; | |||
} | |||
// Grab the data block | |||
byte[] data = new byte[length]; | |||
IOUtils.readFully(value, data); | |||
// Skip over any padding | |||
if (length < 8) { | |||
byte[] padding = new byte[8 - length]; | |||
IOUtils.readFully(value, padding); | |||
} | |||
// Wrap and store | |||
PropertyValue propVal = null; | |||
if (isPointer) { | |||
// We'll match up the chunk later | |||
propVal = new ChunkBasedPropertyValue(prop, flags, data); | |||
} else if (type == Types.NULL) { | |||
propVal = new NullPropertyValue(prop, flags, data); | |||
} else if (type == Types.BOOLEAN) { | |||
propVal = new BooleanPropertyValue(prop, flags, data); | |||
} else if (type == Types.SHORT) { | |||
propVal = new ShortPropertyValue(prop, flags, data); | |||
} else if (type == Types.LONG) { | |||
propVal = new LongPropertyValue(prop, flags, data); | |||
} else if (type == Types.LONG_LONG) { | |||
propVal = new LongLongPropertyValue(prop, flags, data); | |||
} else if (type == Types.FLOAT) { | |||
propVal = new FloatPropertyValue(prop, flags, data); | |||
} else if (type == Types.DOUBLE) { | |||
propVal = new DoublePropertyValue(prop, flags, data); | |||
} else if (type == Types.CURRENCY) { | |||
propVal = new CurrencyPropertyValue(prop, flags, data); | |||
} else if (type == Types.TIME) { | |||
propVal = new TimePropertyValue(prop, flags, data); | |||
} | |||
// TODO Add in the rest of the types | |||
else { | |||
propVal = new PropertyValue(prop, flags, data); | |||
} | |||
if (properties.get(prop) != null) { | |||
logger.log(POILogger.WARN, | |||
"Duplicate values found for " + prop); | |||
} | |||
properties.put(prop, propVal); | |||
} catch (BufferUnderrunException e) { | |||
// Invalid property, ended short | |||
going = false; | |||
} | |||
properties.put(prop, propVal); | |||
} catch (BufferUnderrunException e) { | |||
// Invalid property, ended short | |||
going = false; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
protected void writeProperties(OutputStream out) throws IOException { | |||
// TODO | |||
} | |||
protected void writeProperties(OutputStream out) throws IOException { | |||
// TODO | |||
} | |||
} |
@@ -24,212 +24,228 @@ import org.apache.poi.util.LittleEndian; | |||
import org.apache.poi.util.LocaleUtil; | |||
/** | |||
* An instance of a {@link MAPIProperty} inside a {@link PropertiesChunk}. | |||
* Where the {@link Types} type is a fixed length one, this will contain the | |||
* actual value. | |||
* Where the {@link Types} type is a variable length one, this will contain | |||
* the length of the property, and the value will be in the associated {@link Chunk}. | |||
* An instance of a {@link MAPIProperty} inside a {@link PropertiesChunk}. Where | |||
* the {@link Types} type is a fixed length one, this will contain the actual | |||
* value. Where the {@link Types} type is a variable length one, this will | |||
* contain the length of the property, and the value will be in the associated | |||
* {@link Chunk}. | |||
*/ | |||
public class PropertyValue { | |||
private MAPIProperty property; | |||
private long flags; | |||
protected byte[] data; | |||
public PropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
this.property = property; | |||
this.flags = flags; | |||
this.data = data; | |||
} | |||
public MAPIProperty getProperty() { | |||
return property; | |||
} | |||
/** | |||
* Get the raw value flags. | |||
* TODO Also provide getters for the flag meanings | |||
*/ | |||
public long getFlags() { | |||
return flags; | |||
} | |||
public Object getValue() { | |||
return data; | |||
} | |||
public void setRawValue(byte[] value) { | |||
this.data = value; | |||
} | |||
public String toString() { | |||
Object v = getValue(); | |||
if (v == null) | |||
return "(No value available)"; | |||
if (v instanceof byte[]) { | |||
return ByteChunk.toDebugFriendlyString((byte[])v); | |||
} else { | |||
// Just use the normal toString on the value | |||
return v.toString(); | |||
} | |||
} | |||
public static class NullPropertyValue extends PropertyValue { | |||
public NullPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Void getValue() { | |||
return null; | |||
} | |||
} | |||
public static class BooleanPropertyValue extends PropertyValue { | |||
public BooleanPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Boolean getValue() { | |||
short val = LittleEndian.getShort(data); | |||
return val > 0; | |||
} | |||
public void setValue(boolean value) { | |||
if (data.length != 2) { | |||
data = new byte[2]; | |||
} | |||
if (value) { | |||
LittleEndian.putShort(data, 0, (short)1); | |||
} | |||
} | |||
} | |||
public static class ShortPropertyValue extends PropertyValue { | |||
public ShortPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Short getValue() { | |||
return LittleEndian.getShort(data); | |||
} | |||
public void setValue(short value) { | |||
if (data.length != 2) { | |||
data = new byte[2]; | |||
} | |||
LittleEndian.putShort(data, 0, value); | |||
} | |||
} | |||
public static class LongPropertyValue extends PropertyValue { | |||
public LongPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Integer getValue() { | |||
return LittleEndian.getInt(data); | |||
} | |||
public void setValue(int value) { | |||
if (data.length != 4) { | |||
data = new byte[4]; | |||
} | |||
LittleEndian.putInt(data, 0, value); | |||
} | |||
private MAPIProperty property; | |||
private long flags; | |||
protected byte[] data; | |||
public PropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
this.property = property; | |||
this.flags = flags; | |||
this.data = data; | |||
} | |||
public MAPIProperty getProperty() { | |||
return property; | |||
} | |||
/** | |||
* Get the raw value flags. TODO Also provide getters for the flag meanings | |||
*/ | |||
public long getFlags() { | |||
return flags; | |||
} | |||
public Object getValue() { | |||
return data; | |||
} | |||
public void setRawValue(byte[] value) { | |||
this.data = value; | |||
} | |||
public String toString() { | |||
Object v = getValue(); | |||
if (v == null) | |||
return "(No value available)"; | |||
if (v instanceof byte[]) { | |||
return ByteChunk.toDebugFriendlyString((byte[]) v); | |||
} else { | |||
// Just use the normal toString on the value | |||
return v.toString(); | |||
} | |||
} | |||
public static class NullPropertyValue extends PropertyValue { | |||
public NullPropertyValue(MAPIProperty property, long flags, | |||
byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Void getValue() { | |||
return null; | |||
} | |||
} | |||
public static class BooleanPropertyValue extends PropertyValue { | |||
public BooleanPropertyValue(MAPIProperty property, long flags, | |||
byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Boolean getValue() { | |||
short val = LittleEndian.getShort(data); | |||
return val > 0; | |||
} | |||
public void setValue(boolean value) { | |||
if (data.length != 2) { | |||
data = new byte[2]; | |||
} | |||
if (value) { | |||
LittleEndian.putShort(data, 0, (short) 1); | |||
} | |||
} | |||
} | |||
public static class ShortPropertyValue extends PropertyValue { | |||
public ShortPropertyValue(MAPIProperty property, long flags, | |||
byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Short getValue() { | |||
return LittleEndian.getShort(data); | |||
} | |||
public void setValue(short value) { | |||
if (data.length != 2) { | |||
data = new byte[2]; | |||
} | |||
LittleEndian.putShort(data, 0, value); | |||
} | |||
} | |||
public static class LongPropertyValue extends PropertyValue { | |||
public LongPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Integer getValue() { | |||
return LittleEndian.getInt(data); | |||
} | |||
public void setValue(int value) { | |||
if (data.length != 4) { | |||
data = new byte[4]; | |||
} | |||
LittleEndian.putInt(data, 0, value); | |||
} | |||
} | |||
public static class LongLongPropertyValue extends PropertyValue { | |||
public LongLongPropertyValue(MAPIProperty property, long flags, | |||
byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Long getValue() { | |||
return LittleEndian.getLong(data); | |||
} | |||
public void setValue(long value) { | |||
if (data.length != 8) { | |||
data = new byte[8]; | |||
} | |||
LittleEndian.putLong(data, 0, value); | |||
} | |||
} | |||
public static class FloatPropertyValue extends PropertyValue { | |||
public FloatPropertyValue(MAPIProperty property, long flags, | |||
byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Float getValue() { | |||
return LittleEndian.getFloat(data); | |||
} | |||
public void setValue(float value) { | |||
if (data.length != 4) { | |||
data = new byte[4]; | |||
} | |||
LittleEndian.putFloat(data, 0, value); | |||
} | |||
} | |||
public static class DoublePropertyValue extends PropertyValue { | |||
public DoublePropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Double getValue() { | |||
return LittleEndian.getDouble(data); | |||
} | |||
public void setValue(double value) { | |||
if (data.length != 8) { | |||
data = new byte[8]; | |||
} | |||
LittleEndian.putDouble(data, 0, value); | |||
} | |||
} | |||
/** | |||
* signed 64-bit integer that represents a base ten decimal, with four | |||
* digits to the right of the decimal point | |||
*/ | |||
public static class CurrencyPropertyValue extends PropertyValue { | |||
private static final BigInteger SHIFT = BigInteger.valueOf(10000); | |||
public CurrencyPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public BigInteger getValue() { | |||
long unshifted = LittleEndian.getLong(data); | |||
return BigInteger.valueOf(unshifted).divide(SHIFT); | |||
} | |||
public void setValue(BigInteger value) { | |||
if (data.length != 8) { | |||
data = new byte[8]; | |||
} | |||
long shifted = value.multiply(SHIFT).longValue(); | |||
LittleEndian.putLong(data, 0, shifted); | |||
} | |||
} | |||
/** | |||
* 64-bit integer specifying the number of 100ns periods since Jan 1, 1601 | |||
*/ | |||
public static class TimePropertyValue extends PropertyValue { | |||
private static final long OFFSET = 1000L * 60L * 60L * 24L | |||
* (365L * 369L + 89L); | |||
public TimePropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Calendar getValue() { | |||
long time = LittleEndian.getLong(data); | |||
time = (time / 10 / 1000) - OFFSET; | |||
Calendar timeC = LocaleUtil.getLocaleCalendar(); | |||
timeC.setTimeInMillis(time); | |||
return timeC; | |||
} | |||
public void setValue(Calendar value) { | |||
if (data.length != 8) { | |||
data = new byte[8]; | |||
} | |||
long time = value.getTimeInMillis(); | |||
time = (time + OFFSET) * 10 * 1000; | |||
LittleEndian.putLong(data, 0, time); | |||
} | |||
} | |||
public static class LongLongPropertyValue extends PropertyValue { | |||
public LongLongPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Long getValue() { | |||
return LittleEndian.getLong(data); | |||
} | |||
public void setValue(long value) { | |||
if (data.length != 8) { | |||
data = new byte[8]; | |||
} | |||
LittleEndian.putLong(data, 0, value); | |||
} | |||
} | |||
public static class FloatPropertyValue extends PropertyValue { | |||
public FloatPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Float getValue() { | |||
return LittleEndian.getFloat(data); | |||
} | |||
public void setValue(float value) { | |||
if (data.length != 4) { | |||
data = new byte[4]; | |||
} | |||
LittleEndian.putFloat(data, 0, value); | |||
} | |||
} | |||
public static class DoublePropertyValue extends PropertyValue { | |||
public DoublePropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Double getValue() { | |||
return LittleEndian.getDouble(data); | |||
} | |||
public void setValue(double value) { | |||
if (data.length != 8) { | |||
data = new byte[8]; | |||
} | |||
LittleEndian.putDouble(data, 0, value); | |||
} | |||
} | |||
/** | |||
* signed 64-bit integer that represents a base ten decimal, | |||
* with four digits to the right of the decimal point | |||
*/ | |||
public static class CurrencyPropertyValue extends PropertyValue { | |||
private static final BigInteger SHIFT = BigInteger.valueOf(10000); | |||
public CurrencyPropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public BigInteger getValue() { | |||
long unshifted = LittleEndian.getLong(data); | |||
return BigInteger.valueOf(unshifted).divide(SHIFT); | |||
} | |||
public void setValue(BigInteger value) { | |||
if (data.length != 8) { | |||
data = new byte[8]; | |||
} | |||
long shifted = value.multiply(SHIFT).longValue(); | |||
LittleEndian.putLong(data, 0, shifted); | |||
} | |||
} | |||
/** | |||
* 64-bit integer specifying the number of 100ns periods since Jan 1, 1601 | |||
*/ | |||
public static class TimePropertyValue extends PropertyValue { | |||
private static final long OFFSET = 1000L * 60L * 60L * 24L * (365L * 369L + 89L); | |||
public TimePropertyValue(MAPIProperty property, long flags, byte[] data) { | |||
super(property, flags, data); | |||
} | |||
public Calendar getValue() { | |||
long time = LittleEndian.getLong(data); | |||
time = (time / 10 / 1000) - OFFSET; | |||
Calendar timeC = LocaleUtil.getLocaleCalendar(); | |||
timeC.setTimeInMillis(time); | |||
return timeC; | |||
} | |||
public void setValue(Calendar value) { | |||
if (data.length != 8) { | |||
data = new byte[8]; | |||
} | |||
long time = value.getTimeInMillis(); | |||
time = (time + OFFSET) *10*1000; | |||
LittleEndian.putLong(data, 0, time); | |||
} | |||
} | |||
} |
@@ -27,209 +27,199 @@ import java.util.Map; | |||
import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
/** | |||
* Collection of convenience chunks for the | |||
* Recip(ient) part of an outlook file. | |||
* Collection of convenience chunks for the Recip(ient) part of an outlook file. | |||
* | |||
* If a message has multiple recipients, there will be | |||
* several of these. | |||
* If a message has multiple recipients, there will be several of these. | |||
*/ | |||
public final class RecipientChunks implements ChunkGroupWithProperties { | |||
private static POILogger logger = POILogFactory.getLogger(RecipientChunks.class); | |||
public static final String PREFIX = "__recip_version1.0_#"; | |||
public static final MAPIProperty RECIPIENT_NAME = MAPIProperty.DISPLAY_NAME; | |||
public static final MAPIProperty DELIVERY_TYPE = MAPIProperty.ADDRTYPE; | |||
public static final MAPIProperty RECIPIENT_EMAIL_ADDRESS = MAPIProperty.EMAIL_ADDRESS; | |||
public static final MAPIProperty RECIPIENT_SEARCH = MAPIProperty.SEARCH_KEY; | |||
public static final MAPIProperty RECIPIENT_SMTP_ADDRESS = MAPIProperty.SMTP_ADDRESS; | |||
public static final MAPIProperty RECIPIENT_DISPLAY_NAME = MAPIProperty.RECIPIENT_DISPLAY_NAME; | |||
/** Our 0 based position in the list of recipients */ | |||
public int recipientNumber; | |||
/** TODO */ | |||
public ByteChunk recipientSearchChunk; | |||
/** | |||
* The "name", which could be their name if an | |||
* internal person, or their email address | |||
* if an external person | |||
*/ | |||
public StringChunk recipientNameChunk; | |||
/** | |||
* The email address of the recipient, which | |||
* could be in SMTP or SEARCH format, but | |||
* isn't always present... | |||
*/ | |||
public StringChunk recipientEmailChunk; | |||
/** | |||
* The smtp destination email address of | |||
* the recipient, but isn't always present... | |||
*/ | |||
public StringChunk recipientSMTPChunk; | |||
/** | |||
* Normally EX or SMTP. Will generally affect | |||
* where the email address ends up. | |||
*/ | |||
public StringChunk deliveryTypeChunk; | |||
/** | |||
* The display name of the recipient. | |||
* Normally seems to hold the same value | |||
* as in recipientNameChunk | |||
*/ | |||
public StringChunk recipientDisplayNameChunk; | |||
/** | |||
* Holds the fixed sized properties, and the | |||
* pointers to the data of variable sized ones | |||
*/ | |||
private PropertiesChunk recipientProperties; | |||
public RecipientChunks(String name) { | |||
recipientNumber = -1; | |||
int splitAt = name.lastIndexOf('#'); | |||
if(splitAt > -1) { | |||
String number = name.substring(splitAt+1); | |||
try { | |||
recipientNumber = Integer.parseInt(number, 16); | |||
} catch(NumberFormatException e) { | |||
logger.log(POILogger.ERROR, "Invalid recipient number in name " + name); | |||
} | |||
} | |||
} | |||
/** | |||
* Tries to find their name, | |||
* in whichever chunk holds it. | |||
*/ | |||
public String getRecipientName() { | |||
if(recipientNameChunk != null) { | |||
return recipientNameChunk.getValue(); | |||
} | |||
if(recipientDisplayNameChunk != null) { | |||
return recipientDisplayNameChunk.getValue(); | |||
} | |||
// Can't find it | |||
return null; | |||
} | |||
/** | |||
* Tries to find their email address, in | |||
* whichever chunk holds it given the | |||
* delivery type. | |||
*/ | |||
public String getRecipientEmailAddress() { | |||
// If we have this, it really has the email | |||
if(recipientSMTPChunk != null) { | |||
return recipientSMTPChunk.getValue(); | |||
} | |||
// This might be a real email, or might be | |||
// in CN=... format | |||
if(recipientEmailChunk != null) { | |||
String email = recipientEmailChunk.getValue(); | |||
int cne = email.indexOf("/CN="); | |||
if(cne == -1) { | |||
// Normal smtp address | |||
return email; | |||
} else { | |||
// /O=..../CN=em@ail | |||
return email.substring(cne+4); | |||
} | |||
} | |||
// Might be in the name field, check there | |||
if(recipientNameChunk != null) { | |||
String name = recipientNameChunk.getValue(); | |||
if(name.indexOf('@') > -1) { | |||
// Strip leading and trailing quotes if needed | |||
if(name.startsWith("'") && name.endsWith("'")) { | |||
return name.substring(1, name.length()-1); | |||
private static POILogger logger = POILogFactory.getLogger(RecipientChunks.class); | |||
public static final String PREFIX = "__recip_version1.0_#"; | |||
public static final MAPIProperty RECIPIENT_NAME = MAPIProperty.DISPLAY_NAME; | |||
public static final MAPIProperty DELIVERY_TYPE = MAPIProperty.ADDRTYPE; | |||
public static final MAPIProperty RECIPIENT_EMAIL_ADDRESS = MAPIProperty.EMAIL_ADDRESS; | |||
public static final MAPIProperty RECIPIENT_SEARCH = MAPIProperty.SEARCH_KEY; | |||
public static final MAPIProperty RECIPIENT_SMTP_ADDRESS = MAPIProperty.SMTP_ADDRESS; | |||
public static final MAPIProperty RECIPIENT_DISPLAY_NAME = MAPIProperty.RECIPIENT_DISPLAY_NAME; | |||
/** Our 0 based position in the list of recipients */ | |||
public int recipientNumber; | |||
/** TODO */ | |||
public ByteChunk recipientSearchChunk; | |||
/** | |||
* The "name", which could be their name if an internal person, or their | |||
* email address if an external person | |||
*/ | |||
public StringChunk recipientNameChunk; | |||
/** | |||
* The email address of the recipient, which could be in SMTP or SEARCH | |||
* format, but isn't always present... | |||
*/ | |||
public StringChunk recipientEmailChunk; | |||
/** | |||
* The smtp destination email address of the recipient, but isn't always | |||
* present... | |||
*/ | |||
public StringChunk recipientSMTPChunk; | |||
/** | |||
* Normally EX or SMTP. Will generally affect where the email address ends | |||
* up. | |||
*/ | |||
public StringChunk deliveryTypeChunk; | |||
/** | |||
* The display name of the recipient. Normally seems to hold the same value | |||
* as in recipientNameChunk | |||
*/ | |||
public StringChunk recipientDisplayNameChunk; | |||
/** | |||
* Holds the fixed sized properties, and the pointers to the data of | |||
* variable sized ones | |||
*/ | |||
private PropertiesChunk recipientProperties; | |||
public RecipientChunks(String name) { | |||
recipientNumber = -1; | |||
int splitAt = name.lastIndexOf('#'); | |||
if (splitAt > -1) { | |||
String number = name.substring(splitAt + 1); | |||
try { | |||
recipientNumber = Integer.parseInt(number, 16); | |||
} catch (NumberFormatException e) { | |||
logger.log(POILogger.ERROR, | |||
"Invalid recipient number in name " + name); | |||
} | |||
} | |||
} | |||
/** | |||
* Tries to find their name, in whichever chunk holds it. | |||
*/ | |||
public String getRecipientName() { | |||
if (recipientNameChunk != null) { | |||
return recipientNameChunk.getValue(); | |||
} | |||
if (recipientDisplayNameChunk != null) { | |||
return recipientDisplayNameChunk.getValue(); | |||
} | |||
// Can't find it | |||
return null; | |||
} | |||
/** | |||
* Tries to find their email address, in whichever chunk holds it given the | |||
* delivery type. | |||
*/ | |||
public String getRecipientEmailAddress() { | |||
// If we have this, it really has the email | |||
if (recipientSMTPChunk != null) { | |||
return recipientSMTPChunk.getValue(); | |||
} | |||
// This might be a real email, or might be | |||
// in CN=... format | |||
if (recipientEmailChunk != null) { | |||
String email = recipientEmailChunk.getValue(); | |||
int cne = email.indexOf("/CN="); | |||
if (cne == -1) { | |||
// Normal smtp address | |||
return email; | |||
} else { | |||
// /O=..../CN=em@ail | |||
return email.substring(cne + 4); | |||
} | |||
} | |||
// Might be in the name field, check there | |||
if (recipientNameChunk != null) { | |||
String name = recipientNameChunk.getValue(); | |||
if (name.indexOf('@') > -1) { | |||
// Strip leading and trailing quotes if needed | |||
if (name.startsWith("'") && name.endsWith("'")) { | |||
return name.substring(1, name.length() - 1); | |||
} | |||
return name; | |||
} | |||
} | |||
// Check the search chunk, see if it's | |||
// encoded as a SMTP destination in there. | |||
if (recipientSearchChunk != null) { | |||
String search = recipientSearchChunk.getAs7bitString(); | |||
if (search.indexOf("SMTP:") != -1) { | |||
return search.substring(search.indexOf("SMTP:") + 5); | |||
} | |||
return name; | |||
} | |||
} | |||
// Check the search chunk, see if it's | |||
// encoded as a SMTP destination in there. | |||
if(recipientSearchChunk != null) { | |||
String search = recipientSearchChunk.getAs7bitString(); | |||
if(search.indexOf("SMTP:") != -1) { | |||
return search.substring(search.indexOf("SMTP:") + 5); | |||
} | |||
} | |||
// Can't find it | |||
return null; | |||
} | |||
/** Holds all the chunks that were found. */ | |||
private List<Chunk> allChunks = new ArrayList<Chunk>(); | |||
public Map<MAPIProperty,List<PropertyValue>> getProperties() { | |||
if (recipientProperties != null) { | |||
return recipientProperties.getProperties(); | |||
} | |||
else return Collections.emptyMap(); | |||
} | |||
public Chunk[] getAll() { | |||
return allChunks.toArray(new Chunk[allChunks.size()]); | |||
} | |||
public Chunk[] getChunks() { | |||
return getAll(); | |||
} | |||
/** | |||
* Called by the parser whenever a chunk is found. | |||
*/ | |||
public void record(Chunk chunk) { | |||
if(chunk.getChunkId() == RECIPIENT_SEARCH.id) { | |||
// TODO - parse | |||
recipientSearchChunk = (ByteChunk)chunk; | |||
} | |||
else if(chunk.getChunkId() == RECIPIENT_NAME.id) { | |||
recipientDisplayNameChunk = (StringChunk)chunk; | |||
} | |||
else if(chunk.getChunkId() == RECIPIENT_DISPLAY_NAME.id) { | |||
recipientNameChunk = (StringChunk)chunk; | |||
} | |||
else if(chunk.getChunkId() == RECIPIENT_EMAIL_ADDRESS.id) { | |||
recipientEmailChunk = (StringChunk)chunk; | |||
} | |||
else if(chunk.getChunkId() == RECIPIENT_SMTP_ADDRESS.id) { | |||
recipientSMTPChunk = (StringChunk)chunk; | |||
} | |||
else if(chunk.getChunkId() == DELIVERY_TYPE.id) { | |||
deliveryTypeChunk = (StringChunk)chunk; | |||
} | |||
else if(chunk instanceof PropertiesChunk) { | |||
recipientProperties = (PropertiesChunk) chunk; | |||
} | |||
// And add to the main list | |||
allChunks.add(chunk); | |||
} | |||
public void chunksComplete() { | |||
if (recipientProperties != null) { | |||
recipientProperties.matchVariableSizedPropertiesToChunks(); | |||
} else { | |||
logger.log(POILogger.WARN, "Recipeints Chunk didn't contain a list of properties!"); | |||
} | |||
} | |||
/** | |||
* Orders by the recipient number. | |||
*/ | |||
public static class RecipientChunksSorter implements Comparator<RecipientChunks>, Serializable { | |||
public int compare(RecipientChunks a, RecipientChunks b) { | |||
if(a.recipientNumber < b.recipientNumber) | |||
return -1; | |||
if(a.recipientNumber > b.recipientNumber) | |||
return +1; | |||
return 0; | |||
} | |||
} | |||
} | |||
// Can't find it | |||
return null; | |||
} | |||
/** Holds all the chunks that were found. */ | |||
private List<Chunk> allChunks = new ArrayList<Chunk>(); | |||
public Map<MAPIProperty, List<PropertyValue>> getProperties() { | |||
if (recipientProperties != null) { | |||
return recipientProperties.getProperties(); | |||
} else | |||
return Collections.emptyMap(); | |||
} | |||
public Chunk[] getAll() { | |||
return allChunks.toArray(new Chunk[allChunks.size()]); | |||
} | |||
public Chunk[] getChunks() { | |||
return getAll(); | |||
} | |||
/** | |||
* Called by the parser whenever a chunk is found. | |||
*/ | |||
public void record(Chunk chunk) { | |||
if (chunk.getChunkId() == RECIPIENT_SEARCH.id) { | |||
// TODO - parse | |||
recipientSearchChunk = (ByteChunk) chunk; | |||
} else if (chunk.getChunkId() == RECIPIENT_NAME.id) { | |||
recipientDisplayNameChunk = (StringChunk) chunk; | |||
} else if (chunk.getChunkId() == RECIPIENT_DISPLAY_NAME.id) { | |||
recipientNameChunk = (StringChunk) chunk; | |||
} else if (chunk.getChunkId() == RECIPIENT_EMAIL_ADDRESS.id) { | |||
recipientEmailChunk = (StringChunk) chunk; | |||
} else if (chunk.getChunkId() == RECIPIENT_SMTP_ADDRESS.id) { | |||
recipientSMTPChunk = (StringChunk) chunk; | |||
} else if (chunk.getChunkId() == DELIVERY_TYPE.id) { | |||
deliveryTypeChunk = (StringChunk) chunk; | |||
} else if (chunk instanceof PropertiesChunk) { | |||
recipientProperties = (PropertiesChunk) chunk; | |||
} | |||
// And add to the main list | |||
allChunks.add(chunk); | |||
} | |||
public void chunksComplete() { | |||
if (recipientProperties != null) { | |||
recipientProperties.matchVariableSizedPropertiesToChunks(); | |||
} else { | |||
logger.log(POILogger.WARN, "Recipeints Chunk didn't contain a list of properties!"); | |||
} | |||
} | |||
/** | |||
* Orders by the recipient number. | |||
*/ | |||
public static class RecipientChunksSorter | |||
implements Comparator<RecipientChunks>, Serializable { | |||
public int compare(RecipientChunks a, RecipientChunks b) { | |||
if (a.recipientNumber < b.recipientNumber) | |||
return -1; | |||
if (a.recipientNumber > b.recipientNumber) | |||
return +1; | |||
return 0; | |||
} | |||
} | |||
} |
@@ -24,30 +24,29 @@ import java.io.OutputStream; | |||
import org.apache.poi.util.LittleEndian; | |||
/** | |||
* A {@link PropertiesChunk} for a Storage Properties, such as | |||
* Attachments and Recipients. | |||
* This only has a 8 byte header | |||
* A {@link PropertiesChunk} for a Storage Properties, such as Attachments and | |||
* Recipients. This only has a 8 byte header | |||
*/ | |||
public class StoragePropertiesChunk extends PropertiesChunk { | |||
public StoragePropertiesChunk(ChunkGroup parentGroup) { | |||
super(parentGroup); | |||
} | |||
@Override | |||
public void readValue(InputStream stream) throws IOException { | |||
// 8 bytes of reserved zeros | |||
LittleEndian.readLong(stream); | |||
// Now properties | |||
readProperties(stream); | |||
} | |||
@Override | |||
public void writeValue(OutputStream out) throws IOException { | |||
// 8 bytes of reserved zeros | |||
out.write(new byte[8]); | |||
// Now properties | |||
writeProperties(out); | |||
} | |||
public StoragePropertiesChunk(ChunkGroup parentGroup) { | |||
super(parentGroup); | |||
} | |||
@Override | |||
public void readValue(InputStream stream) throws IOException { | |||
// 8 bytes of reserved zeros | |||
LittleEndian.readLong(stream); | |||
// Now properties | |||
readProperties(stream); | |||
} | |||
@Override | |||
public void writeValue(OutputStream out) throws IOException { | |||
// 8 bytes of reserved zeros | |||
out.write(new byte[8]); | |||
// Now properties | |||
writeProperties(out); | |||
} | |||
} |
@@ -30,120 +30,119 @@ import org.apache.poi.util.StringUtil; | |||
* A Chunk made up of a single string. | |||
*/ | |||
public class StringChunk extends Chunk { | |||
private static final String DEFAULT_ENCODING = "CP1252"; | |||
private String encoding7Bit = DEFAULT_ENCODING; | |||
private byte[] rawValue; | |||
private String value; | |||
/** | |||
* Creates a String Chunk. | |||
*/ | |||
public StringChunk(String namePrefix, int chunkId, MAPIType type) { | |||
super(namePrefix, chunkId, type); | |||
} | |||
/** | |||
* Create a String Chunk, with the specified | |||
* type. | |||
*/ | |||
public StringChunk(int chunkId, MAPIType type) { | |||
super(chunkId, type); | |||
} | |||
/** | |||
* Returns the Encoding that will be used to | |||
* decode any "7 bit" (non unicode) data. | |||
* Most files default to CP1252 | |||
*/ | |||
public String get7BitEncoding() { | |||
return encoding7Bit; | |||
} | |||
/** | |||
* Sets the Encoding that will be used to | |||
* decode any "7 bit" (non unicode) data. | |||
* This doesn't appear to be stored anywhere | |||
* specific in the file, so you may need | |||
* to guess by looking at headers etc | |||
*/ | |||
public void set7BitEncoding(String encoding) { | |||
this.encoding7Bit = encoding; | |||
// Re-read the String if we're a 7 bit one | |||
if(type == Types.ASCII_STRING) { | |||
parseString(); | |||
} | |||
} | |||
public void readValue(InputStream value) throws IOException { | |||
rawValue = IOUtils.toByteArray(value); | |||
parseString(); | |||
} | |||
private void parseString() { | |||
String tmpValue; | |||
if (type == Types.ASCII_STRING) { | |||
tmpValue = parseAs7BitData(rawValue, encoding7Bit); | |||
} else if (type == Types.UNICODE_STRING) { | |||
tmpValue = StringUtil.getFromUnicodeLE(rawValue); | |||
} else { | |||
throw new IllegalArgumentException("Invalid type " + type + " for String Chunk"); | |||
} | |||
// Clean up | |||
this.value = tmpValue.replace("\0", ""); | |||
} | |||
public void writeValue(OutputStream out) throws IOException { | |||
out.write(rawValue); | |||
} | |||
private void storeString() { | |||
if (type == Types.ASCII_STRING) { | |||
rawValue = value.getBytes(Charset.forName(encoding7Bit)); | |||
} else if (type == Types.UNICODE_STRING) { | |||
rawValue = StringUtil.getToUnicodeLE(value); | |||
} else { | |||
throw new IllegalArgumentException("Invalid type " + type + " for String Chunk"); | |||
} | |||
} | |||
/** | |||
* Returns the Text value of the chunk | |||
*/ | |||
public String getValue() { | |||
return this.value; | |||
} | |||
public byte[] getRawValue() { | |||
return this.rawValue; | |||
} | |||
public void setValue(String str) { | |||
this.value = str; | |||
storeString(); | |||
} | |||
public String toString() { | |||
return this.value; | |||
} | |||
/** | |||
* Parses as non-unicode, supposedly 7 bit CP1252 data | |||
* and returns the string that that yields. | |||
*/ | |||
protected static String parseAs7BitData(byte[] data) { | |||
return parseAs7BitData(data, DEFAULT_ENCODING); | |||
} | |||
/** | |||
* Parses as non-unicode, supposedly 7 bit data | |||
* and returns the string that that yields. | |||
*/ | |||
protected static String parseAs7BitData(byte[] data, String encoding) { | |||
// Handle any encoding aliases, where outlook describes it differently | |||
if ("ansi".equals(encoding)) { | |||
encoding = DEFAULT_ENCODING; | |||
} | |||
// Decode | |||
return new String(data, Charset.forName(encoding)); | |||
} | |||
private static final String DEFAULT_ENCODING = "CP1252"; | |||
private String encoding7Bit = DEFAULT_ENCODING; | |||
private byte[] rawValue; | |||
private String value; | |||
/** | |||
* Creates a String Chunk. | |||
*/ | |||
public StringChunk(String namePrefix, int chunkId, MAPIType type) { | |||
super(namePrefix, chunkId, type); | |||
} | |||
/** | |||
* Create a String Chunk, with the specified type. | |||
*/ | |||
public StringChunk(int chunkId, MAPIType type) { | |||
super(chunkId, type); | |||
} | |||
/** | |||
* Returns the Encoding that will be used to decode any "7 bit" (non | |||
* unicode) data. Most files default to CP1252 | |||
*/ | |||
public String get7BitEncoding() { | |||
return encoding7Bit; | |||
} | |||
/** | |||
* Sets the Encoding that will be used to decode any "7 bit" (non unicode) | |||
* data. This doesn't appear to be stored anywhere specific in the file, so | |||
* you may need to guess by looking at headers etc | |||
*/ | |||
public void set7BitEncoding(String encoding) { | |||
this.encoding7Bit = encoding; | |||
// Re-read the String if we're a 7 bit one | |||
if (type == Types.ASCII_STRING) { | |||
parseString(); | |||
} | |||
} | |||
public void readValue(InputStream value) throws IOException { | |||
rawValue = IOUtils.toByteArray(value); | |||
parseString(); | |||
} | |||
private void parseString() { | |||
String tmpValue; | |||
if (type == Types.ASCII_STRING) { | |||
tmpValue = parseAs7BitData(rawValue, encoding7Bit); | |||
} else if (type == Types.UNICODE_STRING) { | |||
tmpValue = StringUtil.getFromUnicodeLE(rawValue); | |||
} else { | |||
throw new IllegalArgumentException("Invalid type " + type + " for String Chunk"); | |||
} | |||
// Clean up | |||
this.value = tmpValue.replace("\0", ""); | |||
} | |||
public void writeValue(OutputStream out) throws IOException { | |||
out.write(rawValue); | |||
} | |||
private void storeString() { | |||
if (type == Types.ASCII_STRING) { | |||
rawValue = value.getBytes(Charset.forName(encoding7Bit)); | |||
} else if (type == Types.UNICODE_STRING) { | |||
rawValue = StringUtil.getToUnicodeLE(value); | |||
} else { | |||
throw new IllegalArgumentException("Invalid type " + type + " for String Chunk"); | |||
} | |||
} | |||
/** | |||
* Returns the Text value of the chunk | |||
*/ | |||
public String getValue() { | |||
return this.value; | |||
} | |||
public byte[] getRawValue() { | |||
return this.rawValue; | |||
} | |||
public void setValue(String str) { | |||
this.value = str; | |||
storeString(); | |||
} | |||
public String toString() { | |||
return this.value; | |||
} | |||
/** | |||
* Parses as non-unicode, supposedly 7 bit CP1252 data and returns the | |||
* string that that yields. | |||
*/ | |||
protected static String parseAs7BitData(byte[] data) { | |||
return parseAs7BitData(data, DEFAULT_ENCODING); | |||
} | |||
/** | |||
* Parses as non-unicode, supposedly 7 bit data and returns the string that | |||
* that yields. | |||
*/ | |||
protected static String parseAs7BitData(byte[] data, String encoding) { | |||
// Handle any encoding aliases, where outlook describes it differently | |||
if ("ansi".equals(encoding)) { | |||
encoding = DEFAULT_ENCODING; | |||
} | |||
// Decode | |||
return new String(data, Charset.forName(encoding)); | |||
} | |||
} |
@@ -23,158 +23,169 @@ import java.util.Map; | |||
/** | |||
* The types list and details are available from | |||
* http://msdn.microsoft.com/en-us/library/microsoft.exchange.data.contenttypes.tnef.tnefpropertytype%28v=EXCHG.140%29.aspx | |||
* http://msdn.microsoft.com/en-us/library/microsoft.exchange.data.contenttypes.tnef.tnefpropertytype%28v=EXCHG.140%29.aspx | |||
*/ | |||
public final class Types { | |||
private static Map<Integer, MAPIType> builtInTypes = new HashMap<Integer, MAPIType>(); | |||
private static Map<Integer, MAPIType> customTypes = new HashMap<Integer, Types.MAPIType>(); | |||
/** Unspecified */ | |||
public static final MAPIType UNSPECIFIED = new MAPIType(0x0000, "Unspecified", -1); | |||
/** Unknown */ | |||
public static final MAPIType UNKNOWN = new MAPIType(-1, "Unknown", -1); | |||
/** Null - NULL property value */ | |||
public static final MAPIType NULL = new MAPIType(0x0001, "Null", 0); | |||
/** I2 - signed 16-bit value */ | |||
public static final MAPIType SHORT = new MAPIType(0x0002, "Short", 2); | |||
/** Long - signed 32-bit value */ | |||
public static final MAPIType LONG = new MAPIType(0x0003, "Long", 4); | |||
/** R4 - 4-byte floating point value */ | |||
public static final MAPIType FLOAT = new MAPIType(0x0004, "Float", 4); | |||
/** Double - floating point double */ | |||
public static final MAPIType DOUBLE = new MAPIType(0x0005, "Double", 8); | |||
/** Currency - signed 64-bit integer that represents a base ten decimal with four digits to the right of the decimal point */ | |||
public static final MAPIType CURRENCY = new MAPIType(0x0006, "Currency", 8); | |||
/** AppTime - application time value */ | |||
public static final MAPIType APP_TIME = new MAPIType(0x0007, "Application Time", 8); | |||
/** Error - 32-bit error value */ | |||
public static final MAPIType ERROR = new MAPIType(0x000A, "Error", 4); | |||
/** Boolean - 16-bit Boolean value. '0' is false. Non-zero is true */ | |||
public static final MAPIType BOOLEAN = new MAPIType(0x000B, "Boolean", 2); | |||
/** Object/Directory - embedded object in a property */ | |||
public static final MAPIType DIRECTORY = new MAPIType(0x000D, "Directory", -1); | |||
/** I8 - 8-byte signed integer */ | |||
public static final MAPIType LONG_LONG = new MAPIType(0x0014, "Long Long", 8); | |||
/** SysTime - FILETIME 64-bit integer specifying the number of 100ns periods since Jan 1, 1601 */ | |||
public static final MAPIType TIME = new MAPIType(0x0040, "Time", 8); | |||
/** ClassId - OLE GUID */ | |||
public static final MAPIType CLS_ID = new MAPIType(0x0048, "CLS ID GUID", 16); | |||
/** Binary - counted byte array */ | |||
public static final MAPIType BINARY = new MAPIType(0x0102, "Binary", -1); | |||
/** | |||
* An 8-bit string, probably in CP1252, but don't quote us... | |||
* Normally used for everything before Outlook 3.0, and some | |||
* fields in Outlook 3.0. | |||
*/ | |||
public static final MAPIType ASCII_STRING = new MAPIType(0x001E, "ASCII String", -1); | |||
/** A string, from Outlook 3.0 onwards. Normally unicode */ | |||
public static final MAPIType UNICODE_STRING = new MAPIType(0x001F, "Unicode String", -1); | |||
/** MultiValued - Value part contains multiple values */ | |||
public static final int MULTIVALUED_FLAG = 0x1000; | |||
public static final class MAPIType { | |||
private final int id; | |||
private final String name; | |||
private final int length; | |||
/** | |||
* Creates a standard, built-in type | |||
*/ | |||
private MAPIType(int id, String name, int length) { | |||
this.id = id; | |||
this.name = name; | |||
this.length = length; | |||
builtInTypes.put(id, this); | |||
} | |||
/** | |||
* Creates a custom type | |||
*/ | |||
private MAPIType(int id, int length) { | |||
this.id = id; | |||
this.name = asCustomName(id); | |||
this.length = length; | |||
customTypes.put(id, this); | |||
} | |||
/** | |||
* Returns the length, in bytes, of values of this type, or | |||
* -1 if it is a variable length type. | |||
*/ | |||
public int getLength() { | |||
return length; | |||
} | |||
/** | |||
* Is this type a fixed-length type, or a variable-length one? | |||
*/ | |||
public boolean isFixedLength() { | |||
return (length != -1); | |||
} | |||
public int getId() { | |||
return id; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
public String toString() { | |||
return id + " / 0x" + asFileEnding() + " - " + name + " @ " + length; | |||
} | |||
/** | |||
* Return the 4 character hex encoded version, | |||
* as used in file endings | |||
*/ | |||
public String asFileEnding() { | |||
return Types.asFileEnding(id); | |||
} | |||
} | |||
public static MAPIType getById(int typeId) { | |||
return builtInTypes.get(typeId); | |||
} | |||
public static String asFileEnding(int type) { | |||
String str = Integer.toHexString(type).toUpperCase(Locale.ROOT); | |||
while(str.length() < 4) { | |||
str = "0" + str; | |||
} | |||
return str; | |||
} | |||
public static String asName(int typeId) { | |||
MAPIType type = builtInTypes.get(typeId); | |||
if (type != null) { | |||
return type.name; | |||
} | |||
return asCustomName(typeId); | |||
} | |||
private static String asCustomName(int typeId) { | |||
return "0x" + Integer.toHexString(typeId); | |||
} | |||
public static MAPIType createCustom(int typeId) { | |||
// Check they're not being silly, and asking for a built-in one... | |||
if (getById(typeId) != null) { | |||
return getById(typeId); | |||
} | |||
// Try to get an existing definition of this | |||
MAPIType type = customTypes.get(typeId); | |||
// If none, do a thread-safe creation | |||
if (type == null) { | |||
synchronized (customTypes) { | |||
type = customTypes.get(typeId); | |||
if (type == null) { | |||
type = new MAPIType(typeId, -1); | |||
private static Map<Integer, MAPIType> builtInTypes = new HashMap<Integer, MAPIType>(); | |||
private static Map<Integer, MAPIType> customTypes = new HashMap<Integer, Types.MAPIType>(); | |||
/** Unspecified */ | |||
public static final MAPIType UNSPECIFIED = new MAPIType(0x0000, | |||
"Unspecified", -1); | |||
/** Unknown */ | |||
public static final MAPIType UNKNOWN = new MAPIType(-1, "Unknown", -1); | |||
/** Null - NULL property value */ | |||
public static final MAPIType NULL = new MAPIType(0x0001, "Null", 0); | |||
/** I2 - signed 16-bit value */ | |||
public static final MAPIType SHORT = new MAPIType(0x0002, "Short", 2); | |||
/** Long - signed 32-bit value */ | |||
public static final MAPIType LONG = new MAPIType(0x0003, "Long", 4); | |||
/** R4 - 4-byte floating point value */ | |||
public static final MAPIType FLOAT = new MAPIType(0x0004, "Float", 4); | |||
/** Double - floating point double */ | |||
public static final MAPIType DOUBLE = new MAPIType(0x0005, "Double", 8); | |||
/** | |||
* Currency - signed 64-bit integer that represents a base ten decimal with | |||
* four digits to the right of the decimal point | |||
*/ | |||
public static final MAPIType CURRENCY = new MAPIType(0x0006, "Currency", 8); | |||
/** AppTime - application time value */ | |||
public static final MAPIType APP_TIME = new MAPIType(0x0007, "Application Time", 8); | |||
/** Error - 32-bit error value */ | |||
public static final MAPIType ERROR = new MAPIType(0x000A, "Error", 4); | |||
/** Boolean - 16-bit Boolean value. '0' is false. Non-zero is true */ | |||
public static final MAPIType BOOLEAN = new MAPIType(0x000B, "Boolean", 2); | |||
/** Object/Directory - embedded object in a property */ | |||
public static final MAPIType DIRECTORY = new MAPIType(0x000D, "Directory", -1); | |||
/** I8 - 8-byte signed integer */ | |||
public static final MAPIType LONG_LONG = new MAPIType(0x0014, "Long Long", 8); | |||
/** | |||
* SysTime - FILETIME 64-bit integer specifying the number of 100ns periods | |||
* since Jan 1, 1601 | |||
*/ | |||
public static final MAPIType TIME = new MAPIType(0x0040, "Time", 8); | |||
/** ClassId - OLE GUID */ | |||
public static final MAPIType CLS_ID = new MAPIType(0x0048, "CLS ID GUID", 16); | |||
/** Binary - counted byte array */ | |||
public static final MAPIType BINARY = new MAPIType(0x0102, "Binary", -1); | |||
/** | |||
* An 8-bit string, probably in CP1252, but don't quote us... Normally used | |||
* for everything before Outlook 3.0, and some fields in Outlook 3.0. | |||
*/ | |||
public static final MAPIType ASCII_STRING = new MAPIType(0x001E, "ASCII String", -1); | |||
/** A string, from Outlook 3.0 onwards. Normally unicode */ | |||
public static final MAPIType UNICODE_STRING = new MAPIType(0x001F, "Unicode String", -1); | |||
/** MultiValued - Value part contains multiple values */ | |||
public static final int MULTIVALUED_FLAG = 0x1000; | |||
public static final class MAPIType { | |||
private final int id; | |||
private final String name; | |||
private final int length; | |||
/** | |||
* Creates a standard, built-in type | |||
*/ | |||
private MAPIType(int id, String name, int length) { | |||
this.id = id; | |||
this.name = name; | |||
this.length = length; | |||
builtInTypes.put(id, this); | |||
} | |||
/** | |||
* Creates a custom type | |||
*/ | |||
private MAPIType(int id, int length) { | |||
this.id = id; | |||
this.name = asCustomName(id); | |||
this.length = length; | |||
customTypes.put(id, this); | |||
} | |||
/** | |||
* Returns the length, in bytes, of values of this type, or -1 if it is | |||
* a variable length type. | |||
*/ | |||
public int getLength() { | |||
return length; | |||
} | |||
/** | |||
* Is this type a fixed-length type, or a variable-length one? | |||
*/ | |||
public boolean isFixedLength() { | |||
return (length != -1); | |||
} | |||
public int getId() { | |||
return id; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
public String toString() { | |||
return id + " / 0x" + asFileEnding() + " - " + name + " @ " | |||
+ length; | |||
} | |||
/** | |||
* Return the 4 character hex encoded version, as used in file endings | |||
*/ | |||
public String asFileEnding() { | |||
return Types.asFileEnding(id); | |||
} | |||
} | |||
public static MAPIType getById(int typeId) { | |||
return builtInTypes.get(typeId); | |||
} | |||
public static String asFileEnding(int type) { | |||
String str = Integer.toHexString(type).toUpperCase(Locale.ROOT); | |||
while (str.length() < 4) { | |||
str = "0" + str; | |||
} | |||
return str; | |||
} | |||
public static String asName(int typeId) { | |||
MAPIType type = builtInTypes.get(typeId); | |||
if (type != null) { | |||
return type.name; | |||
} | |||
return asCustomName(typeId); | |||
} | |||
private static String asCustomName(int typeId) { | |||
return "0x" + Integer.toHexString(typeId); | |||
} | |||
public static MAPIType createCustom(int typeId) { | |||
// Check they're not being silly, and asking for a built-in one... | |||
if (getById(typeId) != null) { | |||
return getById(typeId); | |||
} | |||
// Try to get an existing definition of this | |||
MAPIType type = customTypes.get(typeId); | |||
// If none, do a thread-safe creation | |||
if (type == null) { | |||
synchronized (customTypes) { | |||
type = customTypes.get(typeId); | |||
if (type == null) { | |||
type = new MAPIType(typeId, -1); | |||
} | |||
} | |||
} | |||
} | |||
return type; | |||
} | |||
} | |||
return type; | |||
} | |||
} |