280 lines
11 KiB
Java
280 lines
11 KiB
Java
/* ====================================================================
|
|
Licensed to the Apache Software Foundation (ASF) under one or more
|
|
contributor license agreements. See the NOTICE file distributed with
|
|
this work for additional information regarding copyright ownership.
|
|
The ASF licenses this file to You under the Apache License, Version 2.0
|
|
(the "License"); you may not use this file except in compliance with
|
|
the License. You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
==================================================================== */
|
|
|
|
package org.apache.poi.hsmf.datatypes;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.BooleanPropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.CurrencyPropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.DoublePropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.FloatPropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.LongLongPropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.LongPropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.NullPropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.ShortPropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.PropertyValue.TimePropertyValue;
|
|
import org.apache.poi.hsmf.datatypes.Types.MAPIType;
|
|
import org.apache.poi.util.IOUtils;
|
|
import org.apache.poi.util.LittleEndian;
|
|
import org.apache.poi.util.LittleEndian.BufferUnderrunException;
|
|
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.
|
|
*/
|
|
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);
|
|
|
|
/**
|
|
* 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>();
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
/**
|
|
* <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 {
|
|
logger.log(POILogger.WARN, "No chunk found matching Property " + cVal);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void writeProperties(OutputStream out) throws IOException {
|
|
// TODO
|
|
}
|
|
}
|