mirror of
https://github.com/apache/poi.git
synced 2026-02-27 20:40:08 +08:00
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1809169 13f79535-47bb-0310-9956-ffa450edef68
1045 lines
35 KiB
Java
1045 lines
35 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.hpsf;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
|
|
import org.apache.commons.collections4.bidimap.TreeBidiMap;
|
|
import org.apache.poi.hpsf.wellknown.PropertyIDMap;
|
|
import org.apache.poi.hpsf.wellknown.SectionIDMap;
|
|
import org.apache.poi.util.CodePageUtil;
|
|
import org.apache.poi.util.IOUtils;
|
|
import org.apache.poi.util.LittleEndian;
|
|
import org.apache.poi.util.LittleEndianByteArrayInputStream;
|
|
import org.apache.poi.util.LittleEndianConsts;
|
|
import org.apache.poi.util.POILogFactory;
|
|
import org.apache.poi.util.POILogger;
|
|
|
|
/**
|
|
* Represents a section in a {@link PropertySet}.
|
|
*/
|
|
public class Section {
|
|
//arbitrarily selected; may need to increase
|
|
private static final int MAX_RECORD_LENGTH = 100_000;
|
|
|
|
private static final POILogger LOG = POILogFactory.getLogger(Section.class);
|
|
|
|
/**
|
|
* Maps property IDs to section-private PID strings. These
|
|
* strings can be found in the property with ID 0.
|
|
*/
|
|
private Map<Long,String> dictionary;
|
|
|
|
/**
|
|
* The section's format ID, {@link #getFormatID}.
|
|
*/
|
|
private ClassID formatID;
|
|
|
|
/**
|
|
* Contains the bytes making out the section. This byte array is
|
|
* established when the section's size is calculated and can be reused
|
|
* later. If the array is empty, the section was modified and the bytes need to be regenerated.
|
|
*/
|
|
private final ByteArrayOutputStream sectionBytes = new ByteArrayOutputStream();
|
|
|
|
/**
|
|
* The offset of the section in the stream.
|
|
*/
|
|
private final long _offset;
|
|
|
|
/**
|
|
* This section's properties.
|
|
*/
|
|
private final Map<Long,Property> properties = new LinkedHashMap<>();
|
|
|
|
/**
|
|
* This member is {@code true} if the last call to {@link
|
|
* #getPropertyIntValue} or {@link #getProperty} tried to access a
|
|
* property that was not available, else {@code false}.
|
|
*/
|
|
private boolean wasNull;
|
|
|
|
/**
|
|
* Creates an empty {@link Section}.
|
|
*/
|
|
public Section() {
|
|
this._offset = -1;
|
|
}
|
|
|
|
/**
|
|
* Constructs a {@code Section} by doing a deep copy of an
|
|
* existing {@code Section}. All nested {@code Property}
|
|
* instances, will be their mutable counterparts in the new
|
|
* {@code MutableSection}.
|
|
*
|
|
* @param s The section set to copy
|
|
*/
|
|
public Section(final Section s) {
|
|
this._offset = -1;
|
|
setFormatID(s.getFormatID());
|
|
for (Property p : s.properties.values()) {
|
|
properties.put(p.getID(), new Property(p));
|
|
}
|
|
setDictionary(s.getDictionary());
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Creates a {@link Section} instance from a byte array.
|
|
*
|
|
* @param src Contains the complete property set stream.
|
|
* @param offset The position in the stream that points to the
|
|
* section's format ID.
|
|
*
|
|
* @exception UnsupportedEncodingException if the section's codepage is not
|
|
* supported.
|
|
*/
|
|
public Section(final byte[] src, final int offset) throws UnsupportedEncodingException {
|
|
/*
|
|
* Read the format ID.
|
|
*/
|
|
formatID = new ClassID(src, offset);
|
|
|
|
/*
|
|
* Read the offset from the stream's start and positions to
|
|
* the section header.
|
|
*/
|
|
int offFix = (int)LittleEndian.getUInt(src, offset + ClassID.LENGTH);
|
|
|
|
// some input files have a invalid (padded?) offset, which need to be fixed
|
|
// search for beginning of size field
|
|
if (src[offFix] == 0) {
|
|
for (int i=0; i<3 && src[offFix] == 0; i++,offFix++);
|
|
// cross check with propertyCount field and the property list field
|
|
for (int i=0; i<3 && (src[offFix+3] != 0 || src[offFix+7] != 0 || src[offFix+11] != 0); i++,offFix--);
|
|
}
|
|
|
|
this._offset = offFix;
|
|
|
|
LittleEndianByteArrayInputStream leis = new LittleEndianByteArrayInputStream(src, offFix);
|
|
|
|
/*
|
|
* Read the section length.
|
|
*/
|
|
int size = (int)Math.min(leis.readUInt(), src.length-_offset);
|
|
|
|
/*
|
|
* Read the number of properties.
|
|
*/
|
|
final int propertyCount = (int)leis.readUInt();
|
|
|
|
/*
|
|
* Read the properties. The offset is positioned at the first
|
|
* entry of the property list. There are two problems:
|
|
*
|
|
* 1. For each property we have to find out its length. In the
|
|
* property list we find each property's ID and its offset relative
|
|
* to the section's beginning. Unfortunately the properties in the
|
|
* property list need not to be in ascending order, so it is not
|
|
* possible to calculate the length as
|
|
* (offset of property(i+1) - offset of property(i)). Before we can
|
|
* that we first have to sort the property list by ascending offsets.
|
|
*
|
|
* 2. We have to read the property with ID 1 before we read other
|
|
* properties, at least before other properties containing strings.
|
|
* The reason is that property 1 specifies the codepage. If it is
|
|
* 1200, all strings are in Unicode. In other words: Before we can
|
|
* read any strings we have to know whether they are in Unicode or
|
|
* not. Unfortunately property 1 is not guaranteed to be the first in
|
|
* a section.
|
|
*
|
|
* The algorithm below reads the properties in two passes: The first
|
|
* one looks for property ID 1 and extracts the codepage number. The
|
|
* seconds pass reads the other properties.
|
|
*/
|
|
/* Pass 1: Read the property list. */
|
|
final TreeBidiMap<Long,Long> offset2Id = new TreeBidiMap<>();
|
|
for (int i = 0; i < propertyCount; i++) {
|
|
/* Read the property ID. */
|
|
long id = (int)leis.readUInt();
|
|
|
|
/* Offset from the section's start. */
|
|
long off = (int)leis.readUInt();
|
|
|
|
offset2Id.put(off, id);
|
|
}
|
|
|
|
Long cpOffset = offset2Id.getKey((long)PropertyIDMap.PID_CODEPAGE);
|
|
|
|
/* Look for the codepage. */
|
|
int codepage = -1;
|
|
if (cpOffset != null) {
|
|
/* Read the property's value type. It must be VT_I2. */
|
|
leis.setReadIndex((int)(this._offset + cpOffset));
|
|
final long type = leis.readUInt();
|
|
|
|
if (type != Variant.VT_I2) {
|
|
throw new HPSFRuntimeException
|
|
("Value type of property ID 1 is not VT_I2 but " + type + ".");
|
|
}
|
|
|
|
/* Read the codepage number. */
|
|
codepage = leis.readUShort();
|
|
setCodepage(codepage);
|
|
}
|
|
|
|
|
|
/* Pass 2: Read all properties - including the codepage property,
|
|
* if available. */
|
|
for (Map.Entry<Long,Long> me : offset2Id.entrySet()) {
|
|
long off = me.getKey();
|
|
long id = me.getValue();
|
|
|
|
if (id == PropertyIDMap.PID_CODEPAGE) {
|
|
continue;
|
|
}
|
|
|
|
int pLen = propLen(offset2Id, off, size);
|
|
leis.setReadIndex((int)(this._offset + off));
|
|
|
|
if (id == PropertyIDMap.PID_DICTIONARY) {
|
|
leis.mark(100000);
|
|
if (!readDictionary(leis, pLen, codepage)) {
|
|
// there was an error reading the dictionary, maybe because the pid (0) was used wrong
|
|
// try reading a property instead
|
|
leis.reset();
|
|
try {
|
|
// fix id
|
|
id = Math.max(PropertyIDMap.PID_MAX, offset2Id.inverseBidiMap().lastKey())+1;
|
|
setProperty(new Property(id, leis, pLen, codepage));
|
|
} catch (RuntimeException e) {
|
|
LOG.log(POILogger.INFO, "Dictionary fallback failed - ignoring property");
|
|
}
|
|
}
|
|
} else {
|
|
setProperty(new Property(id, leis, pLen, codepage));
|
|
}
|
|
}
|
|
|
|
sectionBytes.write(src, (int)_offset, size);
|
|
padSectionBytes();
|
|
}
|
|
|
|
/**
|
|
* Retrieves the length of the given property (by key)
|
|
*
|
|
* @param offset2Id the offset to id map
|
|
* @param entryOffset the current entry key
|
|
* @param maxSize the maximum offset/size of the section stream
|
|
* @return the length of the current property
|
|
*/
|
|
private static int propLen(
|
|
TreeBidiMap<Long,Long> offset2Id,
|
|
Long entryOffset,
|
|
long maxSize) {
|
|
Long nextKey = offset2Id.nextKey(entryOffset);
|
|
long begin = entryOffset;
|
|
long end = (nextKey != null) ? nextKey : maxSize;
|
|
return (int)(end - begin);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the format ID. The format ID is the "type" of the
|
|
* section. For example, if the format ID of the first {@link
|
|
* Section} contains the bytes specified by
|
|
* {@code org.apache.poi.hpsf.wellknown.SectionIDMap.SUMMARY_INFORMATION_ID}
|
|
* the section (and thus the property set) is a SummaryInformation.
|
|
*
|
|
* @return The format ID
|
|
*/
|
|
public ClassID getFormatID() {
|
|
return formatID;
|
|
}
|
|
|
|
/**
|
|
* Sets the section's format ID.
|
|
*
|
|
* @param formatID The section's format ID
|
|
*/
|
|
public void setFormatID(final ClassID formatID) {
|
|
this.formatID = formatID;
|
|
}
|
|
|
|
/**
|
|
* Sets the section's format ID.
|
|
*
|
|
* @param formatID The section's format ID as a byte array. It components
|
|
* are in big-endian format.
|
|
*/
|
|
public void setFormatID(final byte[] formatID) {
|
|
ClassID fid = getFormatID();
|
|
if (fid == null) {
|
|
fid = new ClassID();
|
|
setFormatID(fid);
|
|
}
|
|
fid.setBytes(formatID);
|
|
}
|
|
|
|
/**
|
|
* Returns the offset of the section in the stream.
|
|
*
|
|
* @return The offset of the section in the stream.
|
|
*/
|
|
public long getOffset() {
|
|
return _offset;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of properties in this section.
|
|
*
|
|
* @return The number of properties in this section.
|
|
*/
|
|
public int getPropertyCount() {
|
|
return properties.size();
|
|
}
|
|
|
|
/**
|
|
* Returns this section's properties.
|
|
*
|
|
* @return This section's properties.
|
|
*/
|
|
public Property[] getProperties() {
|
|
return properties.values().toArray(new Property[properties.size()]);
|
|
}
|
|
|
|
/**
|
|
* Sets this section's properties. Any former values are overwritten.
|
|
*
|
|
* @param properties This section's new properties.
|
|
*/
|
|
public void setProperties(final Property[] properties) {
|
|
this.properties.clear();
|
|
for (Property p : properties) {
|
|
setProperty(p);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the value of the property with the specified ID. If
|
|
* the property is not available, {@code null} is returned
|
|
* and a subsequent call to {@link #wasNull} will return
|
|
* {@code true}.
|
|
*
|
|
* @param id The property's ID
|
|
*
|
|
* @return The property's value
|
|
*/
|
|
public Object getProperty(final long id) {
|
|
wasNull = !properties.containsKey(id);
|
|
return (wasNull) ? null : properties.get(id).getValue();
|
|
}
|
|
|
|
/**
|
|
* Sets the string value of the property with the specified ID.
|
|
*
|
|
* @param id The property's ID
|
|
* @param value The property's value.
|
|
*/
|
|
public void setProperty(final int id, final String value) {
|
|
setProperty(id, Variant.VT_LPSTR, value);
|
|
}
|
|
|
|
/**
|
|
* Sets the int value of the property with the specified ID.
|
|
*
|
|
* @param id The property's ID
|
|
* @param value The property's value.
|
|
*
|
|
* @see #setProperty(int, long, Object)
|
|
* @see #getProperty
|
|
*/
|
|
public void setProperty(final int id, final int value) {
|
|
setProperty(id, Variant.VT_I4, Integer.valueOf(value));
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Sets the long value of the property with the specified ID.
|
|
*
|
|
* @param id The property's ID
|
|
* @param value The property's value.
|
|
*
|
|
* @see #setProperty(int, long, Object)
|
|
* @see #getProperty
|
|
*/
|
|
public void setProperty(final int id, final long value) {
|
|
setProperty(id, Variant.VT_I8, Long.valueOf(value));
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Sets the boolean value of the property with the specified ID.
|
|
*
|
|
* @param id The property's ID
|
|
* @param value The property's value.
|
|
*
|
|
* @see #setProperty(int, long, Object)
|
|
* @see #getProperty
|
|
*/
|
|
public void setProperty(final int id, final boolean value) {
|
|
setProperty(id, Variant.VT_BOOL, Boolean.valueOf(value));
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Sets the value and the variant type of the property with the
|
|
* specified ID. If a property with this ID is not yet present in
|
|
* the section, it will be added. An already present property with
|
|
* the specified ID will be overwritten. A default mapping will be
|
|
* used to choose the property's type.
|
|
*
|
|
* @param id The property's ID.
|
|
* @param variantType The property's variant type.
|
|
* @param value The property's value.
|
|
*
|
|
* @see #setProperty(int, String)
|
|
* @see #getProperty
|
|
* @see Variant
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
public void setProperty(final int id, final long variantType, final Object value) {
|
|
setProperty(new Property(id, variantType, value));
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Sets a property.
|
|
*
|
|
* @param p The property to be set.
|
|
*
|
|
* @see #setProperty(int, long, Object)
|
|
* @see #getProperty
|
|
* @see Variant
|
|
*/
|
|
public void setProperty(final Property p) {
|
|
Property old = properties.get(p.getID());
|
|
if (old == null || !old.equals(p)) {
|
|
properties.put(p.getID(), p);
|
|
sectionBytes.reset();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a property.
|
|
*
|
|
* @param id The property ID.
|
|
* @param value The property's value. The value's class must be one of those
|
|
* supported by HPSF.
|
|
*/
|
|
public void setProperty(final int id, final Object value) {
|
|
if (value instanceof String) {
|
|
setProperty(id, (String) value);
|
|
} else if (value instanceof Long) {
|
|
setProperty(id, ((Long) value).longValue());
|
|
} else if (value instanceof Integer) {
|
|
setProperty(id, ((Integer) value).intValue());
|
|
} else if (value instanceof Short) {
|
|
setProperty(id, ((Short) value).intValue());
|
|
} else if (value instanceof Boolean) {
|
|
setProperty(id, ((Boolean) value).booleanValue());
|
|
} else if (value instanceof Date) {
|
|
setProperty(id, Variant.VT_FILETIME, value);
|
|
} else {
|
|
throw new HPSFRuntimeException(
|
|
"HPSF does not support properties of type " +
|
|
value.getClass().getName() + ".");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the value of the numeric property with the specified
|
|
* ID. If the property is not available, 0 is returned. A
|
|
* subsequent call to {@link #wasNull} will return
|
|
* {@code true} to let the caller distinguish that case from
|
|
* a real property value of 0.
|
|
*
|
|
* @param id The property's ID
|
|
*
|
|
* @return The property's value
|
|
*/
|
|
protected int getPropertyIntValue(final long id) {
|
|
final Number i;
|
|
final Object o = getProperty(id);
|
|
if (o == null) {
|
|
return 0;
|
|
}
|
|
if (!(o instanceof Long || o instanceof Integer)) {
|
|
throw new HPSFRuntimeException
|
|
("This property is not an integer type, but " +
|
|
o.getClass().getName() + ".");
|
|
}
|
|
i = (Number) o;
|
|
return i.intValue();
|
|
}
|
|
|
|
/**
|
|
* Returns the value of the boolean property with the specified
|
|
* ID. If the property is not available, {@code false} is
|
|
* returned. A subsequent call to {@link #wasNull} will return
|
|
* {@code true} to let the caller distinguish that case from
|
|
* a real property value of {@code false}.
|
|
*
|
|
* @param id The property's ID
|
|
*
|
|
* @return The property's value
|
|
*/
|
|
protected boolean getPropertyBooleanValue(final int id) {
|
|
final Boolean b = (Boolean) getProperty(id);
|
|
if (b == null) {
|
|
return false;
|
|
}
|
|
return b.booleanValue();
|
|
}
|
|
|
|
/**
|
|
* Sets the value of the boolean property with the specified
|
|
* ID.
|
|
*
|
|
* @param id The property's ID
|
|
* @param value The property's value
|
|
*
|
|
* @see #setProperty(int, long, Object)
|
|
* @see #getProperty
|
|
* @see Variant
|
|
*/
|
|
protected void setPropertyBooleanValue(final int id, final boolean value) {
|
|
setProperty(id, Variant.VT_BOOL, Boolean.valueOf(value));
|
|
}
|
|
|
|
/**
|
|
* @return the section's size in bytes.
|
|
*/
|
|
public int getSize() {
|
|
int size = sectionBytes.size();
|
|
if (size > 0) {
|
|
return size;
|
|
}
|
|
try {
|
|
return calcSize();
|
|
} catch (HPSFRuntimeException ex) {
|
|
throw ex;
|
|
} catch (Exception ex) {
|
|
throw new HPSFRuntimeException(ex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculates the section's size. It is the sum of the lengths of the
|
|
* section's header (8), the properties list (16 times the number of
|
|
* properties) and the properties themselves.
|
|
*
|
|
* @return the section's length in bytes.
|
|
* @throws WritingNotSupportedException
|
|
* @throws IOException
|
|
*/
|
|
private int calcSize() throws WritingNotSupportedException, IOException {
|
|
sectionBytes.reset();
|
|
write(sectionBytes);
|
|
padSectionBytes();
|
|
return sectionBytes.size();
|
|
}
|
|
|
|
private void padSectionBytes() {
|
|
byte[] padArray = { 0, 0, 0 };
|
|
/* Pad to multiple of 4 bytes so that even the Windows shell (explorer)
|
|
* shows custom properties. */
|
|
int pad = (4 - (sectionBytes.size() & 0x3)) & 0x3;
|
|
sectionBytes.write(padArray, 0, pad);
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks whether the property which the last call to {@link
|
|
* #getPropertyIntValue} or {@link #getProperty} tried to access
|
|
* was available or not. This information might be important for
|
|
* callers of {@link #getPropertyIntValue} since the latter
|
|
* returns 0 if the property does not exist. Using {@link
|
|
* #wasNull} the caller can distiguish this case from a property's
|
|
* real value of 0.
|
|
*
|
|
* @return {@code true} if the last call to {@link
|
|
* #getPropertyIntValue} or {@link #getProperty} tried to access a
|
|
* property that was not available, else {@code false}.
|
|
*/
|
|
public boolean wasNull() {
|
|
return wasNull;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Returns the PID string associated with a property ID. The ID
|
|
* is first looked up in the {@link Section}'s private
|
|
* dictionary. If it is not found there, the method calls {@link
|
|
* SectionIDMap#getPIDString}.
|
|
*
|
|
* @param pid The property ID
|
|
*
|
|
* @return The property ID's string value
|
|
*/
|
|
public String getPIDString(final long pid) {
|
|
String s = null;
|
|
Map<Long,String> dic = getDictionary();
|
|
if (dic != null) {
|
|
s = dic.get(pid);
|
|
}
|
|
if (s == null) {
|
|
s = SectionIDMap.getPIDString(getFormatID(), pid);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/**
|
|
* Removes all properties from the section including 0 (dictionary) and
|
|
* 1 (codepage).
|
|
*/
|
|
public void clear() {
|
|
for (Property p : getProperties()) {
|
|
removeProperty(p.getID());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks whether this section is equal to another object. The result is
|
|
* {@code false} if one of the the following conditions holds:
|
|
*
|
|
* <ul>
|
|
*
|
|
* <li>The other object is not a {@link Section}.
|
|
*
|
|
* <li>The format IDs of the two sections are not equal.
|
|
*
|
|
* <li>The sections have a different number of properties. However,
|
|
* properties with ID 1 (codepage) are not counted.
|
|
*
|
|
* <li>The other object is not a {@link Section}.
|
|
*
|
|
* <li>The properties have different values. The order of the properties
|
|
* is irrelevant.
|
|
*
|
|
* </ul>
|
|
*
|
|
* @param o The object to compare this section with
|
|
* @return {@code true} if the objects are equal, {@code false} if
|
|
* not
|
|
*/
|
|
@Override
|
|
public boolean equals(final Object o) {
|
|
if (!(o instanceof Section)) {
|
|
return false;
|
|
}
|
|
final Section s = (Section) o;
|
|
if (!s.getFormatID().equals(getFormatID())) {
|
|
return false;
|
|
}
|
|
|
|
/* Compare all properties except the dictionary (id 0) and
|
|
* the codepage (id 1 / ignored) as they must be handled specially. */
|
|
Set<Long> propIds = new HashSet<>(properties.keySet());
|
|
propIds.addAll(s.properties.keySet());
|
|
propIds.remove(0L);
|
|
propIds.remove(1L);
|
|
|
|
for (Long id : propIds) {
|
|
Property p1 = properties.get(id);
|
|
Property p2 = s.properties.get(id);
|
|
if (p1 == null || p2 == null || !p1.equals(p2)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* If the dictionaries are unequal the sections are unequal. */
|
|
Map<Long,String> d1 = getDictionary();
|
|
Map<Long,String> d2 = s.getDictionary();
|
|
|
|
return (d1 == null && d2 == null) || (d1 != null && d2 != null && d1.equals(d2));
|
|
}
|
|
|
|
/**
|
|
* Removes a property.
|
|
*
|
|
* @param id The ID of the property to be removed
|
|
*/
|
|
public void removeProperty(final long id) {
|
|
if (properties.remove(id) != null) {
|
|
sectionBytes.reset();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes this section into an output stream.<p>
|
|
*
|
|
* Internally this is done by writing into three byte array output
|
|
* streams: one for the properties, one for the property list and one for
|
|
* the section as such. The two former are appended to the latter when they
|
|
* have received all their data.
|
|
*
|
|
* @param out The stream to write into.
|
|
*
|
|
* @return The number of bytes written, i.e. the section's size.
|
|
* @exception IOException if an I/O error occurs
|
|
* @exception WritingNotSupportedException if HPSF does not yet support
|
|
* writing a property's variant type.
|
|
*/
|
|
public int write(final OutputStream out) throws WritingNotSupportedException, IOException {
|
|
/* Check whether we have already generated the bytes making out the
|
|
* section. */
|
|
if (sectionBytes.size() > 0) {
|
|
sectionBytes.writeTo(out);
|
|
return sectionBytes.size();
|
|
}
|
|
|
|
/* Writing the section's dictionary it tricky. If there is a dictionary
|
|
* (property 0) the codepage property (property 1) must be set, too. */
|
|
int codepage = getCodepage();
|
|
if (codepage == -1) {
|
|
String msg =
|
|
"The codepage property is not set although a dictionary is present. "+
|
|
"Defaulting to ISO-8859-1.";
|
|
LOG.log(POILogger.WARN, msg);
|
|
codepage = Property.DEFAULT_CODEPAGE;
|
|
}
|
|
|
|
/* The properties are written to this stream. */
|
|
final ByteArrayOutputStream propertyStream = new ByteArrayOutputStream();
|
|
|
|
/* The property list is established here. After each property that has
|
|
* been written to "propertyStream", a property list entry is written to
|
|
* "propertyListStream". */
|
|
final ByteArrayOutputStream propertyListStream = new ByteArrayOutputStream();
|
|
|
|
/* Maintain the current position in the list. */
|
|
int position = 0;
|
|
|
|
/* Increase the position variable by the size of the property list so
|
|
* that it points behind the property list and to the beginning of the
|
|
* properties themselves. */
|
|
position += 2 * LittleEndianConsts.INT_SIZE + getPropertyCount() * 2 * LittleEndianConsts.INT_SIZE;
|
|
|
|
/* Write the properties and the property list into their respective
|
|
* streams: */
|
|
for (Property p : properties.values()) {
|
|
final long id = p.getID();
|
|
|
|
/* Write the property list entry. */
|
|
LittleEndian.putUInt(id, propertyListStream);
|
|
LittleEndian.putUInt(position, propertyListStream);
|
|
|
|
/* If the property ID is not equal 0 we write the property and all
|
|
* is fine. However, if it equals 0 we have to write the section's
|
|
* dictionary which has an implicit type only and an explicit
|
|
* value. */
|
|
if (id != 0) {
|
|
/* Write the property and update the position to the next
|
|
* property. */
|
|
position += p.write(propertyStream, codepage);
|
|
} else {
|
|
if (codepage == -1) {
|
|
throw new IllegalPropertySetDataException("Codepage (property 1) is undefined.");
|
|
}
|
|
position += writeDictionary(propertyStream, codepage);
|
|
}
|
|
}
|
|
|
|
/* Write the section: */
|
|
int streamLength = LittleEndianConsts.INT_SIZE * 2 + propertyListStream.size() + propertyStream.size();
|
|
|
|
/* Write the section's length: */
|
|
LittleEndian.putInt(streamLength, out);
|
|
|
|
/* Write the section's number of properties: */
|
|
LittleEndian.putInt(getPropertyCount(), out);
|
|
|
|
/* Write the property list: */
|
|
propertyListStream.writeTo(out);
|
|
|
|
/* Write the properties: */
|
|
propertyStream.writeTo(out);
|
|
|
|
return streamLength;
|
|
}
|
|
|
|
/**
|
|
* Reads a dictionary.
|
|
*
|
|
* @param leis The byte stream containing the bytes making out the dictionary.
|
|
* @param length The dictionary contains at most this many bytes.
|
|
* @param codepage The codepage of the string values.
|
|
*
|
|
* @return {@code true} if dictionary was read successful, {@code false} otherwise
|
|
*
|
|
* @throws UnsupportedEncodingException if the dictionary's codepage is not
|
|
* (yet) supported.
|
|
*/
|
|
private boolean readDictionary(LittleEndianByteArrayInputStream leis, final int length, final int codepage)
|
|
throws UnsupportedEncodingException {
|
|
Map<Long,String> dic = new HashMap<>();
|
|
|
|
/*
|
|
* Read the number of dictionary entries.
|
|
*/
|
|
final long nrEntries = leis.readUInt();
|
|
|
|
long id = -1;
|
|
boolean isCorrupted = false;
|
|
for (int i = 0; i < nrEntries; i++) {
|
|
String errMsg =
|
|
"The property set's dictionary contains bogus data. "
|
|
+ "All dictionary entries starting with the one with ID "
|
|
+ id + " will be ignored.";
|
|
|
|
/* The key. */
|
|
id = leis.readUInt();
|
|
|
|
/* The value (a string). The length is the either the
|
|
* number of (two-byte) characters if the character set is Unicode
|
|
* or the number of bytes if the character set is not Unicode.
|
|
* The length includes terminating 0x00 bytes which we have to strip
|
|
* off to create a Java string. */
|
|
long sLength = leis.readUInt();
|
|
|
|
/* Read the string - Strip 0x00 characters from the end of the string. */
|
|
int cp = (codepage == -1) ? Property.DEFAULT_CODEPAGE : codepage;
|
|
int nrBytes = (int)((sLength-1) * (cp == CodePageUtil.CP_UNICODE ? 2 : 1));
|
|
if (nrBytes > 0xFFFFFF) {
|
|
LOG.log(POILogger.WARN, errMsg);
|
|
isCorrupted = true;
|
|
break;
|
|
}
|
|
|
|
try {
|
|
byte buf[] = IOUtils.safelyAllocate(nrBytes, MAX_RECORD_LENGTH);
|
|
leis.readFully(buf, 0, nrBytes);
|
|
final String str = CodePageUtil.getStringFromCodePage(buf, 0, nrBytes, cp);
|
|
|
|
int pad = 1;
|
|
if (cp == CodePageUtil.CP_UNICODE) {
|
|
pad = 2+((4 - ((nrBytes+2) & 0x3)) & 0x3);
|
|
}
|
|
leis.skip(pad);
|
|
|
|
dic.put(id, str);
|
|
} catch (RuntimeException ex) {
|
|
LOG.log(POILogger.WARN, errMsg, ex);
|
|
isCorrupted = true;
|
|
break;
|
|
}
|
|
}
|
|
setDictionary(dic);
|
|
return !isCorrupted;
|
|
}
|
|
|
|
|
|
/**
|
|
* Writes the section's dictionary.
|
|
*
|
|
* @param out The output stream to write to.
|
|
* @param dictionary The dictionary.
|
|
* @param codepage The codepage to be used to write the dictionary items.
|
|
* @return The number of bytes written
|
|
* @exception IOException if an I/O exception occurs.
|
|
*/
|
|
private int writeDictionary(final OutputStream out, final int codepage)
|
|
throws IOException {
|
|
final byte padding[] = new byte[4];
|
|
Map<Long,String> dic = getDictionary();
|
|
|
|
LittleEndian.putUInt(dic.size(), out);
|
|
int length = LittleEndianConsts.INT_SIZE;
|
|
for (Map.Entry<Long,String> ls : dic.entrySet()) {
|
|
|
|
LittleEndian.putUInt(ls.getKey(), out);
|
|
length += LittleEndianConsts.INT_SIZE;
|
|
|
|
String value = ls.getValue()+"\0";
|
|
LittleEndian.putUInt( value.length(), out );
|
|
length += LittleEndianConsts.INT_SIZE;
|
|
|
|
byte bytes[] = CodePageUtil.getBytesInCodePage(value, codepage);
|
|
out.write(bytes);
|
|
length += bytes.length;
|
|
|
|
if (codepage == CodePageUtil.CP_UNICODE) {
|
|
int pad = (4 - (length & 0x3)) & 0x3;
|
|
out.write(padding, 0, pad);
|
|
length += pad;
|
|
}
|
|
}
|
|
|
|
int pad = (4 - (length & 0x3)) & 0x3;
|
|
out.write(padding, 0, pad);
|
|
length += pad;
|
|
|
|
return length;
|
|
}
|
|
|
|
/**
|
|
* Sets the section's dictionary. All keys in the dictionary must be
|
|
* {@link java.lang.Long} instances, all values must be
|
|
* {@link java.lang.String}s. This method overwrites the properties with IDs
|
|
* 0 and 1 since they are reserved for the dictionary and the dictionary's
|
|
* codepage. Setting these properties explicitly might have surprising
|
|
* effects. An application should never do this but always use this
|
|
* method.
|
|
*
|
|
* @param dictionary The dictionary
|
|
*
|
|
* @exception IllegalPropertySetDataException if the dictionary's key and
|
|
* value types are not correct.
|
|
*
|
|
* @see Section#getDictionary()
|
|
*/
|
|
public void setDictionary(final Map<Long,String> dictionary) throws IllegalPropertySetDataException {
|
|
if (dictionary != null) {
|
|
if (this.dictionary == null) {
|
|
this.dictionary = new TreeMap<>();
|
|
}
|
|
this.dictionary.putAll(dictionary);
|
|
|
|
/* If the codepage property (ID 1) for the strings (keys and values)
|
|
* used in the dictionary is not yet defined, set it to ISO-8859-1. */
|
|
int cp = getCodepage();
|
|
if (cp == -1) {
|
|
setCodepage(Property.DEFAULT_CODEPAGE);
|
|
}
|
|
|
|
/* Set the dictionary property (ID 0). Please note that the second
|
|
* parameter in the method call below is unused because dictionaries
|
|
* don't have a type. */
|
|
setProperty(PropertyIDMap.PID_DICTIONARY, -1, dictionary);
|
|
} else {
|
|
/* Setting the dictionary to null means to remove property 0.
|
|
* However, it does not mean to remove property 1 (codepage). */
|
|
removeProperty(PropertyIDMap.PID_DICTIONARY);
|
|
this.dictionary = null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* @see Object#hashCode()
|
|
*/
|
|
@Override
|
|
public int hashCode() {
|
|
long hashCode = 0;
|
|
hashCode += getFormatID().hashCode();
|
|
final Property[] pa = getProperties();
|
|
for (int i = 0; i < pa.length; i++) {
|
|
hashCode += pa[i].hashCode();
|
|
}
|
|
return (int) (hashCode & 0x0ffffffffL);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* @see Object#toString()
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
return toString(null);
|
|
}
|
|
|
|
public String toString(PropertyIDMap idMap) {
|
|
final StringBuffer b = new StringBuffer();
|
|
final Property[] pa = getProperties();
|
|
b.append("\n\n\n");
|
|
b.append(getClass().getName());
|
|
b.append('[');
|
|
b.append("formatID: ");
|
|
b.append(getFormatID());
|
|
b.append(", offset: ");
|
|
b.append(getOffset());
|
|
b.append(", propertyCount: ");
|
|
b.append(getPropertyCount());
|
|
b.append(", size: ");
|
|
b.append(getSize());
|
|
b.append(", properties: [\n");
|
|
int codepage = getCodepage();
|
|
if (codepage == -1) {
|
|
codepage = Property.DEFAULT_CODEPAGE;
|
|
}
|
|
for (Property p : pa) {
|
|
b.append(p.toString(codepage, idMap));
|
|
b.append(",\n");
|
|
}
|
|
b.append(']');
|
|
b.append(']');
|
|
return b.toString();
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Gets the section's dictionary. A dictionary allows an application to
|
|
* use human-readable property names instead of numeric property IDs. It
|
|
* contains mappings from property IDs to their associated string
|
|
* values. The dictionary is stored as the property with ID 0. The codepage
|
|
* for the strings in the dictionary is defined by property with ID 1.
|
|
*
|
|
* @return the dictionary or {@code null} if the section does not have
|
|
* a dictionary.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
public Map<Long,String> getDictionary() {
|
|
if (dictionary == null) {
|
|
dictionary = (Map<Long,String>) getProperty(PropertyIDMap.PID_DICTIONARY);
|
|
}
|
|
|
|
return dictionary;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Gets the section's codepage, if any.
|
|
*
|
|
* @return The section's codepage if one is defined, else -1.
|
|
*/
|
|
public int getCodepage() {
|
|
final Integer codepage = (Integer) getProperty(PropertyIDMap.PID_CODEPAGE);
|
|
return (codepage == null) ? -1 : codepage.intValue();
|
|
}
|
|
|
|
/**
|
|
* Sets the codepage.
|
|
*
|
|
* @param codepage the codepage
|
|
*/
|
|
public void setCodepage(final int codepage) {
|
|
setProperty(PropertyIDMap.PID_CODEPAGE, Variant.VT_I2, codepage);
|
|
}
|
|
}
|