/*
* Copyright (c) 2010 Google Inc.
*
* Licensed 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 com.google.api.client.xml;
import com.google.api.client.util.Beta;
import com.google.api.client.util.Data;
import com.google.api.client.util.DateTime;
import com.google.api.client.util.FieldInfo;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.Types;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* {@link Beta} <br/>
* Thread-safe XML namespace dictionary that provides a one-to-one map of namespace alias to URI.
*
* <p>
* Implementation is thread-safe. For maximum efficiency, applications should use a single
* globally-shared instance of the XML namespace dictionary.
* </p>
*
* <p>
* A namespace alias is uniquely mapped to a single namespace URI, and a namespace URI is uniquely
* mapped to a single namespace alias. In other words, it is not possible to have duplicates.
* </p>
*
* <p>
* Sample usage:
* </p>
*
* <pre>{@code
static final XmlNamespaceDictionary DICTIONARY = new XmlNamespaceDictionary()
.set("", "http://www.w3.org/2005/Atom")
.set("activity", "http://activitystrea.ms/spec/1.0/")
.set("georss", "http://www.georss.org/georss")
.set("media", "http://search.yahoo.com/mrss/")
.set("thr", "http://purl.org/syndication/thread/1.0");
*}</pre>
*
* @since 1.0
* @author Yaniv Inbar
*/
@Beta
public final class XmlNamespaceDictionary {
/**
* Map from XML namespace alias (or {@code ""} for the default namespace) to XML namespace URI.
*/
private final HashMap<String, String> namespaceAliasToUriMap = new HashMap<String, String>();
/**
* Map from XML namespace URI to XML namespace alias (or {@code ""} for the default namespace).
*/
private final HashMap<String, String> namespaceUriToAliasMap = new HashMap<String, String>();
/**
* Returns the namespace alias (or {@code ""} for the default namespace) for the given namespace
* URI.
*
* @param uri namespace URI
* @since 1.3
*/
public synchronized String getAliasForUri(String uri) {
return namespaceUriToAliasMap.get(Preconditions.checkNotNull(uri));
}
/**
* Returns the namespace URI for the given namespace alias (or {@code ""} for the default
* namespace).
*
* @param alias namespace alias (or {@code ""} for the default namespace)
* @since 1.3
*/
public synchronized String getUriForAlias(String alias) {
return namespaceAliasToUriMap.get(Preconditions.checkNotNull(alias));
}
/**
* Returns an unmodified set of map entries for the map from namespace alias (or {@code ""} for
* the default namespace) to namespace URI.
*
* @since 1.3
*/
public synchronized Map<String, String> getAliasToUriMap() {
return Collections.unmodifiableMap(namespaceAliasToUriMap);
}
/**
* Returns an unmodified set of map entries for the map from namespace URI to namespace alias (or
* {@code ""} for the default namespace).
*
* @since 1.3
*/
public synchronized Map<String, String> getUriToAliasMap() {
return Collections.unmodifiableMap(namespaceUriToAliasMap);
}
/**
* Adds a namespace of the given alias and URI.
*
* <p>
* If the uri is {@code null}, the namespace alias will be removed. Similarly, if the alias is
* {@code null}, the namespace URI will be removed. Otherwise, if the alias is already mapped to a
* different URI, it will be remapped to the new URI. Similarly, if a URI is already mapped to a
* different alias, it will be remapped to the new alias.
* </p>
*
* @param alias alias or {@code null} to remove the namespace URI
* @param uri namespace URI or {@code null} to remove the namespace alias
* @return this namespace dictionary
* @since 1.3
*/
public synchronized XmlNamespaceDictionary set(String alias, String uri) {
String previousUri = null;
String previousAlias = null;
if (uri == null) {
if (alias != null) {
previousUri = namespaceAliasToUriMap.remove(alias);
}
} else if (alias == null) {
previousAlias = namespaceUriToAliasMap.remove(uri);
} else {
previousUri = namespaceAliasToUriMap.put(
Preconditions.checkNotNull(alias), Preconditions.checkNotNull(uri));
if (!uri.equals(previousUri)) {
previousAlias = namespaceUriToAliasMap.put(uri, alias);
} else {
previousUri = null;
}
}
if (previousUri != null) {
namespaceUriToAliasMap.remove(previousUri);
}
if (previousAlias != null) {
namespaceAliasToUriMap.remove(previousAlias);
}
return this;
}
/**
* Shows a debug string representation of an element data object of key/value pairs.
*
* @param element element data object ({@link GenericXml}, {@link Map}, or any object with public
* fields)
* @param elementName optional XML element local name prefixed by its namespace alias -- for
* example {@code "atom:entry"} -- or {@code null} to make up something
*/
public String toStringOf(String elementName, Object element) {
try {
StringWriter writer = new StringWriter();
XmlSerializer serializer = Xml.createSerializer();
serializer.setOutput(writer);
serialize(serializer, elementName, element, false);
return writer.toString();
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Shows a debug string representation of an element data object of key/value pairs.
*
* @param element element data object ({@link GenericXml}, {@link Map}, or any object with public
* fields)
* @param elementNamespaceUri XML namespace URI or {@code null} for no namespace
* @param elementLocalName XML local name
* @throws IOException I/O exception
*/
public void serialize(
XmlSerializer serializer, String elementNamespaceUri, String elementLocalName, Object element)
throws IOException {
serialize(serializer, elementNamespaceUri, elementLocalName, element, true);
}
/**
* Shows a debug string representation of an element data object of key/value pairs.
*
* @param element element data object ({@link GenericXml}, {@link Map}, or any object with public
* fields)
* @param elementName XML element local name prefixed by its namespace alias
* @throws IOException I/O exception
*/
public void serialize(XmlSerializer serializer, String elementName, Object element)
throws IOException {
serialize(serializer, elementName, element, true);
}
private void serialize(XmlSerializer serializer, String elementNamespaceUri,
String elementLocalName, Object element, boolean errorOnUnknown) throws IOException {
String elementAlias = elementNamespaceUri == null ? null : getAliasForUri(elementNamespaceUri);
startDoc(serializer, element, errorOnUnknown, elementAlias).serialize(
serializer, elementNamespaceUri, elementLocalName);
serializer.endDocument();
}
private void serialize(
XmlSerializer serializer, String elementName, Object element, boolean errorOnUnknown)
throws IOException {
String elementAlias = "";
if (elementName != null) {
int colon = elementName.indexOf(':');
if (colon != -1) {
elementAlias = elementName.substring(0, colon);
}
}
startDoc(serializer, element, errorOnUnknown, elementAlias).serialize(serializer, elementName);
serializer.endDocument();
}
private ElementSerializer startDoc(
XmlSerializer serializer, Object element, boolean errorOnUnknown, String elementAlias)
throws IOException {
serializer.startDocument(null, null);
SortedSet<String> aliases = new TreeSet<String>();
computeAliases(element, aliases);
if (elementAlias != null) {
aliases.add(elementAlias);
}
for (String alias : aliases) {
String uri = getNamespaceUriForAliasHandlingUnknown(errorOnUnknown, alias);
serializer.setPrefix(alias, uri);
}
return new ElementSerializer(element, errorOnUnknown);
}
private void computeAliases(Object element, SortedSet<String> aliases) {
for (Map.Entry<String, Object> entry : Data.mapOf(element).entrySet()) {
Object value = entry.getValue();
if (value != null) {
String name = entry.getKey();
if (!Xml.TEXT_CONTENT.equals(name)) {
int colon = name.indexOf(':');
boolean isAttribute = name.charAt(0) == '@';
if (colon != -1 || !isAttribute) {
String alias = colon == -1 ? "" : name.substring(name.charAt(0) == '@' ? 1 : 0, colon);
aliases.add(alias);
}
Class<?> valueClass = value.getClass();
if (!isAttribute && !Data.isPrimitive(valueClass)) {
if (value instanceof Iterable<?> || valueClass.isArray()) {
for (Object subValue : Types.iterableOf(value)) {
computeAliases(subValue, aliases);
}
} else {
computeAliases(value, aliases);
}
}
}
}
}
}
/**
* Returns the namespace URI to use for serialization for a given namespace alias, possibly using
* a predictable made-up namespace URI if the alias is not recognized.
*
* <p>
* Specifically, if the namespace alias is not recognized, the namespace URI returned will be
* {@code "http://unknown/"} plus the alias, unless {@code errorOnUnknown} is {@code true} in
* which case it will throw an {@link IllegalArgumentException}.
* </p>
*
* @param errorOnUnknown whether to thrown an exception if the namespace alias is not recognized
* @param alias namespace alias
* @return namespace URI, using a predictable made-up namespace URI if the namespace alias is not
* recognized
* @throws IllegalArgumentException if the namespace alias is not recognized and {@code
* errorOnUnkown} is {@code true}
*/
String getNamespaceUriForAliasHandlingUnknown(boolean errorOnUnknown, String alias) {
String result = getUriForAlias(alias);
if (result == null) {
Preconditions.checkArgument(
!errorOnUnknown, "unrecognized alias: %s", alias.length() == 0 ? "(default)" : alias);
return "http://unknown/" + alias;
}
return result;
}
/**
* Returns the namespace alias to use for a given namespace URI, throwing an exception if the
* namespace URI can be found in this dictionary.
*
* @param namespaceUri namespace URI
* @throws IllegalArgumentException if the namespace URI is not found in this dictionary
*/
String getNamespaceAliasForUriErrorOnUnknown(String namespaceUri) {
String result = getAliasForUri(namespaceUri);
Preconditions.checkArgument(result != null,
"invalid XML: no alias declared for namesapce <%s>; "
+ "work-around by setting XML namepace directly by calling the set method of %s",
namespaceUri, XmlNamespaceDictionary.class.getName());
return result;
}
@Beta
class ElementSerializer {
private final boolean errorOnUnknown;
Object textValue = null;
final List<String> attributeNames = new ArrayList<String>();
final List<Object> attributeValues = new ArrayList<Object>();
final List<String> subElementNames = new ArrayList<String>();
final List<Object> subElementValues = new ArrayList<Object>();
ElementSerializer(Object elementValue, boolean errorOnUnknown) {
this.errorOnUnknown = errorOnUnknown;
Class<?> valueClass = elementValue.getClass();
if (Data.isPrimitive(valueClass) && !Data.isNull(elementValue)) {
textValue = elementValue;
} else {
for (Map.Entry<String, Object> entry : Data.mapOf(elementValue).entrySet()) {
Object fieldValue = entry.getValue();
if (fieldValue != null && !Data.isNull(fieldValue)) {
String fieldName = entry.getKey();
if (Xml.TEXT_CONTENT.equals(fieldName)) {
textValue = fieldValue;
} else if (fieldName.charAt(0) == '@') {
attributeNames.add(fieldName.substring(1));
attributeValues.add(fieldValue);
} else {
subElementNames.add(fieldName);
subElementValues.add(fieldValue);
}
}
}
}
}
void serialize(XmlSerializer serializer, String elementName) throws IOException {
String elementLocalName = null;
String elementNamespaceUri = null;
if (elementName != null) {
int colon = elementName.indexOf(':');
elementLocalName = elementName.substring(colon + 1);
String alias = colon == -1 ? "" : elementName.substring(0, colon);
elementNamespaceUri = getNamespaceUriForAliasHandlingUnknown(errorOnUnknown, alias);
}
serialize(serializer, elementNamespaceUri, elementLocalName);
}
void serialize(XmlSerializer serializer, String elementNamespaceUri, String elementLocalName)
throws IOException {
boolean errorOnUnknown = this.errorOnUnknown;
if (elementLocalName == null) {
if (errorOnUnknown) {
throw new IllegalArgumentException("XML name not specified");
}
elementLocalName = "unknownName";
}
serializer.startTag(elementNamespaceUri, elementLocalName);
// attributes
int num = attributeNames.size();
for (int i = 0; i < num; i++) {
String attributeName = attributeNames.get(i);
int colon = attributeName.indexOf(':');
String attributeLocalName = attributeName.substring(colon + 1);
String attributeNamespaceUri = colon == -1 ? null : getNamespaceUriForAliasHandlingUnknown(
errorOnUnknown, attributeName.substring(0, colon));
serializer.attribute(
attributeNamespaceUri, attributeLocalName, toSerializedValue(attributeValues.get(i)));
}
// text
if (textValue != null) {
serializer.text(toSerializedValue(textValue));
}
// elements
num = subElementNames.size();
for (int i = 0; i < num; i++) {
Object subElementValue = subElementValues.get(i);
String subElementName = subElementNames.get(i);
Class<? extends Object> valueClass = subElementValue.getClass();
if (subElementValue instanceof Iterable<?> || valueClass.isArray()) {
for (Object subElement : Types.iterableOf(subElementValue)) {
if (subElement != null && !Data.isNull(subElement)) {
new ElementSerializer(subElement, errorOnUnknown).serialize(
serializer, subElementName);
}
}
} else {
new ElementSerializer(subElementValue, errorOnUnknown).serialize(
serializer, subElementName);
}
}
serializer.endTag(elementNamespaceUri, elementLocalName);
}
}
static String toSerializedValue(Object value) {
if (value instanceof Float) {
Float f = (Float) value;
if (f.floatValue() == Float.POSITIVE_INFINITY) {
return "INF";
}
if (f.floatValue() == Float.NEGATIVE_INFINITY) {
return "-INF";
}
}
if (value instanceof Double) {
Double d = (Double) value;
if (d.doubleValue() == Double.POSITIVE_INFINITY) {
return "INF";
}
if (d.doubleValue() == Double.NEGATIVE_INFINITY) {
return "-INF";
}
}
if (value instanceof String || value instanceof Number || value instanceof Boolean) {
return value.toString();
}
if (value instanceof DateTime) {
return ((DateTime) value).toStringRfc3339();
}
if (value instanceof Enum<?>) {
return FieldInfo.of((Enum<?>) value).getName();
}
throw new IllegalArgumentException("unrecognized value type: " + value.getClass());
}
}