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@1808403 13f79535-47bb-0310-9956-ffa450edef68
408 lines
13 KiB
Java
408 lines
13 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.UnsupportedEncodingException;
|
|
import java.math.BigInteger;
|
|
import java.nio.charset.Charset;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
import org.apache.commons.collections4.bidimap.TreeBidiMap;
|
|
import org.apache.poi.hpsf.wellknown.PropertyIDMap;
|
|
import org.apache.poi.util.CodePageUtil;
|
|
import org.apache.poi.util.POILogFactory;
|
|
import org.apache.poi.util.POILogger;
|
|
|
|
/**
|
|
* Maintains the instances of {@link CustomProperty} that belong to a
|
|
* {@link DocumentSummaryInformation}. The class maintains the names of the
|
|
* custom properties in a dictionary. It implements the {@link Map} interface
|
|
* and by this provides a simplified view on custom properties: A property's
|
|
* name is the key that maps to a typed value. This implementation hides
|
|
* property IDs from the developer and regards the property names as keys to
|
|
* typed values.<p>
|
|
*
|
|
* While this class provides a simple API to custom properties, it ignores
|
|
* the fact that not names, but IDs are the real keys to properties. Under the
|
|
* hood this class maintains a 1:1 relationship between IDs and names. Therefore
|
|
* you should not use this class to process property sets with several IDs
|
|
* mapping to the same name or with properties without a name: the result will
|
|
* contain only a subset of the original properties. If you really need to deal
|
|
* such property sets, use HPSF's low-level access methods.<p>
|
|
*
|
|
* An application can call the {@link #isPure} method to check whether a
|
|
* property set parsed by {@link CustomProperties} is still pure (i.e.
|
|
* unmodified) or whether one or more properties have been dropped.<p>
|
|
*
|
|
* This class is not thread-safe; concurrent access to instances of this
|
|
* class must be synchronized.<p>
|
|
*
|
|
* While this class is roughly HashMap<Long,CustomProperty>, that's the
|
|
* internal representation. To external calls, it should appear as
|
|
* HashMap<String,Object> mapping between Names and Custom Property Values.
|
|
*/
|
|
public class CustomProperties implements Map<String,Object> {
|
|
private static final POILogger LOG = POILogFactory.getLogger(CustomProperties.class);
|
|
|
|
/**
|
|
* The custom properties
|
|
*/
|
|
private final HashMap<Long,CustomProperty> props = new HashMap<Long,CustomProperty>();
|
|
|
|
/**
|
|
* Maps property IDs to property names and vice versa.
|
|
*/
|
|
private final TreeBidiMap<Long,String> dictionary = new TreeBidiMap<Long,String>();
|
|
|
|
/**
|
|
* Tells whether this object is pure or not.
|
|
*/
|
|
private boolean isPure = true;
|
|
|
|
private int codepage = -1;
|
|
|
|
/**
|
|
* Puts a {@link CustomProperty} into this map. It is assumed that the
|
|
* {@link CustomProperty} already has a valid ID. Otherwise use
|
|
* {@link #put(CustomProperty)}.
|
|
*
|
|
* @param name the property name
|
|
* @param cp the property
|
|
*
|
|
* @return the previous property stored under this name
|
|
*/
|
|
public CustomProperty put(final String name, final CustomProperty cp) {
|
|
if (name == null) {
|
|
/* Ignoring a property without a name. */
|
|
isPure = false;
|
|
return null;
|
|
}
|
|
|
|
if (!name.equals(cp.getName())) {
|
|
throw new IllegalArgumentException("Parameter \"name\" (" + name +
|
|
") and custom property's name (" + cp.getName() +
|
|
") do not match.");
|
|
}
|
|
|
|
checkCodePage(name);
|
|
|
|
/* Register name and ID in the dictionary. Mapping in both directions is possible. If there is already a */
|
|
props.remove(dictionary.getKey(name));
|
|
dictionary.put(cp.getID(), name);
|
|
|
|
/* Put the custom property into this map. */
|
|
return props.put(cp.getID(), cp);
|
|
}
|
|
|
|
/**
|
|
* Adds a named property.
|
|
*
|
|
* @param key The property's name.
|
|
* @param value The property's value.
|
|
* @return the property that was stored under the specified name before, or
|
|
* {@code null} if there was no such property before.
|
|
*/
|
|
@Override
|
|
public Object put(String key, Object value) {
|
|
int variantType;
|
|
if (value instanceof String) {
|
|
variantType = Variant.VT_LPSTR;
|
|
} else if (value instanceof Short) {
|
|
variantType = Variant.VT_I2;
|
|
} else if (value instanceof Integer) {
|
|
variantType = Variant.VT_I4;
|
|
} else if (value instanceof Long) {
|
|
variantType = Variant.VT_I8;
|
|
} else if (value instanceof Float) {
|
|
variantType = Variant.VT_R4;
|
|
} else if (value instanceof Double) {
|
|
variantType = Variant.VT_R8;
|
|
} else if (value instanceof Boolean) {
|
|
variantType = Variant.VT_BOOL;
|
|
} else if (value instanceof BigInteger
|
|
&& ((BigInteger)value).bitLength() <= 64
|
|
&& ((BigInteger)value).compareTo(BigInteger.ZERO) >= 0) {
|
|
variantType = Variant.VT_UI8;
|
|
} else if (value instanceof Date) {
|
|
variantType = Variant.VT_FILETIME;
|
|
} else {
|
|
throw new IllegalStateException("unsupported datatype - currently String,Short,Integer,Long,Float,Double,Boolean,BigInteger(unsigned long),Date can be processed.");
|
|
}
|
|
final Property p = new Property(-1, variantType, value);
|
|
return put(new CustomProperty(p, key));
|
|
}
|
|
|
|
/**
|
|
* Gets a named value from the custom properties - only works for keys of type String
|
|
*
|
|
* @param key the name of the value to get
|
|
* @return the value or {@code null} if a value with the specified
|
|
* name is not found in the custom properties.
|
|
*/
|
|
@Override
|
|
public Object get(final Object key) {
|
|
final Long id = dictionary.getKey(key);
|
|
final CustomProperty cp = props.get(id);
|
|
return cp != null ? cp.getValue() : null;
|
|
}
|
|
|
|
/**
|
|
* Removes a custom property - only works for keys of type String
|
|
* @param key The name of the custom property to remove
|
|
* @return The removed property or {@code null} if the specified property was not found.
|
|
*/
|
|
@Override
|
|
public CustomProperty remove(Object key) {
|
|
final Long id = dictionary.removeValue(key);
|
|
return props.remove(id);
|
|
}
|
|
|
|
@Override
|
|
public int size() {
|
|
return props.size();
|
|
}
|
|
|
|
@Override
|
|
public boolean isEmpty() {
|
|
return props.isEmpty();
|
|
}
|
|
|
|
@Override
|
|
public void clear() {
|
|
props.clear();
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return props.hashCode();
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (!(obj instanceof CustomProperties)) {
|
|
return false;
|
|
}
|
|
return props.equals(((CustomProperties)obj).props);
|
|
}
|
|
|
|
@Override
|
|
public void putAll(Map<? extends String, ? extends Object> m) {
|
|
for (Map.Entry<? extends String, ? extends Object> me : m.entrySet()) {
|
|
put(me.getKey(), me.getValue());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return the list of properties
|
|
*/
|
|
public List<CustomProperty> properties() {
|
|
List<CustomProperty> list = new ArrayList<CustomProperty>(props.size());
|
|
for (Long l : dictionary.keySet()) {
|
|
list.add(props.get(l));
|
|
}
|
|
return Collections.unmodifiableList(list);
|
|
}
|
|
|
|
/**
|
|
* @return the list of property values - use {@link #properties()} for the wrapped values
|
|
*/
|
|
@Override
|
|
public Collection<Object> values() {
|
|
List<Object> list = new ArrayList<Object>(props.size());
|
|
for (Long l : dictionary.keySet()) {
|
|
list.add(props.get(l).getValue());
|
|
}
|
|
return Collections.unmodifiableCollection(list);
|
|
}
|
|
|
|
@Override
|
|
public Set<Entry<String, Object>> entrySet() {
|
|
Map<String,Object> set = new LinkedHashMap<String,Object>(props.size());
|
|
for (Entry<Long,String> se : dictionary.entrySet()) {
|
|
set.put(se.getValue(), props.get(se.getKey()).getValue());
|
|
}
|
|
return Collections.unmodifiableSet(set.entrySet());
|
|
}
|
|
|
|
/**
|
|
* Returns a set of all the names of our custom properties.
|
|
* Equivalent to {@link #nameSet()}
|
|
*
|
|
* @return a set of all the names of our custom properties
|
|
*/
|
|
@Override
|
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
public Set keySet() {
|
|
return Collections.unmodifiableSet(dictionary.values());
|
|
}
|
|
|
|
/**
|
|
* Returns a set of all the names of our custom properties
|
|
*
|
|
* @return a set of all the names of our custom properties
|
|
*/
|
|
public Set<String> nameSet() {
|
|
return Collections.unmodifiableSet(dictionary.values());
|
|
}
|
|
|
|
/**
|
|
* Returns a set of all the IDs of our custom properties
|
|
*
|
|
* @return a set of all the IDs of our custom properties
|
|
*/
|
|
public Set<Long> idSet() {
|
|
return Collections.unmodifiableSet(dictionary.keySet());
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the codepage.
|
|
*
|
|
* @param codepage the codepage
|
|
*/
|
|
public void setCodepage(final int codepage) {
|
|
this.codepage = codepage;
|
|
}
|
|
|
|
/**
|
|
* Gets the codepage.
|
|
*
|
|
* @return the codepage or -1 if the codepage is undefined.
|
|
*/
|
|
public int getCodepage() {
|
|
return codepage;
|
|
}
|
|
|
|
/**
|
|
* <p>Gets the dictionary which contains IDs and names of the named custom
|
|
* properties.
|
|
*
|
|
* @return the dictionary.
|
|
*/
|
|
Map<Long,String> getDictionary() {
|
|
return dictionary;
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks against both String Name and Long ID
|
|
*/
|
|
@Override
|
|
public boolean containsKey(Object key) {
|
|
return ((key instanceof Long && dictionary.containsKey(key)) || dictionary.containsValue(key));
|
|
}
|
|
|
|
/**
|
|
* Checks against both the property, and its values.
|
|
*/
|
|
@Override
|
|
public boolean containsValue(Object value) {
|
|
if(value instanceof CustomProperty) {
|
|
return props.containsValue(value);
|
|
}
|
|
|
|
for(CustomProperty cp : props.values()) {
|
|
if(cp.getValue() == value) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Tells whether this {@link CustomProperties} instance is pure or one or
|
|
* more properties of the underlying low-level property set has been
|
|
* dropped.
|
|
*
|
|
* @return {@code true} if the {@link CustomProperties} is pure, else
|
|
* {@code false}.
|
|
*/
|
|
public boolean isPure() {
|
|
return isPure;
|
|
}
|
|
|
|
/**
|
|
* Sets the purity of the custom property set.
|
|
*
|
|
* @param isPure the purity
|
|
*/
|
|
public void setPure(final boolean isPure) {
|
|
this.isPure = isPure;
|
|
}
|
|
|
|
/**
|
|
* Puts a {@link CustomProperty} that has not yet a valid ID into this
|
|
* map. The method will allocate a suitable ID for the custom property:
|
|
*
|
|
* <ul>
|
|
* <li>If there is already a property with the same name, take the ID
|
|
* of that property.
|
|
*
|
|
* <li>Otherwise find the highest ID and use its value plus one.
|
|
* </ul>
|
|
*
|
|
* @param customProperty
|
|
* @return If there was already a property with the same name, the old property
|
|
* @throws ClassCastException
|
|
*/
|
|
private Object put(final CustomProperty customProperty) throws ClassCastException {
|
|
final String name = customProperty.getName();
|
|
|
|
/* Check whether a property with this name is in the map already. */
|
|
final Long oldId = (name == null) ? null : dictionary.getKey(name);
|
|
if (oldId != null) {
|
|
customProperty.setID(oldId);
|
|
} else {
|
|
long lastKey = (dictionary.isEmpty()) ? 0 : dictionary.lastKey();
|
|
long nextKey = Math.max(lastKey,PropertyIDMap.PID_MAX)+1;
|
|
customProperty.setID(nextKey);
|
|
}
|
|
return this.put(name, customProperty);
|
|
}
|
|
|
|
private void checkCodePage(String value) {
|
|
int cp = getCodepage();
|
|
if (cp == -1) {
|
|
cp = Property.DEFAULT_CODEPAGE;
|
|
}
|
|
if (cp == CodePageUtil.CP_UNICODE) {
|
|
return;
|
|
}
|
|
String cps = "";
|
|
try {
|
|
cps = CodePageUtil.codepageToEncoding(cp, false);
|
|
} catch (UnsupportedEncodingException e) {
|
|
LOG.log(POILogger.ERROR, "Codepage '"+cp+"' can't be found.");
|
|
}
|
|
if (!cps.isEmpty() && Charset.forName(cps).newEncoder().canEncode(value)) {
|
|
return;
|
|
}
|
|
LOG.log(POILogger.DEBUG, "Charset '"+cps+"' can't encode '"+value+"' - switching to unicode.");
|
|
setCodepage(CodePageUtil.CP_UNICODE);
|
|
}
|
|
}
|