/google-http-client/src/main/java/com/google/api/client/xml/XmlNamespaceDictionary.java

https://code.google.com/p/google-http-java-client/ · Java · 437 lines · 270 code · 26 blank · 141 comment · 87 complexity · 86967e56d9ee6fa41e8ec256a6d225f9 MD5 · raw file

  1. /*
  2. * Copyright (c) 2010 Google Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
  5. * in compliance with the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License
  10. * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
  11. * or implied. See the License for the specific language governing permissions and limitations under
  12. * the License.
  13. */
  14. package com.google.api.client.xml;
  15. import com.google.api.client.util.Data;
  16. import com.google.api.client.util.DateTime;
  17. import com.google.api.client.util.FieldInfo;
  18. import com.google.api.client.util.Types;
  19. import com.google.common.base.Preconditions;
  20. import org.xmlpull.v1.XmlSerializer;
  21. import java.io.IOException;
  22. import java.io.StringWriter;
  23. import java.util.ArrayList;
  24. import java.util.Collections;
  25. import java.util.HashMap;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.SortedSet;
  29. import java.util.TreeSet;
  30. /**
  31. * Thread-safe XML namespace dictionary that provides a one-to-one map of namespace alias to URI.
  32. *
  33. * <p>
  34. * Implementation is thread-safe. For maximum efficiency, applications should use a single
  35. * globally-shared instance of the XML namespace dictionary.
  36. * </p>
  37. *
  38. * <p>
  39. * A namespace alias is uniquely mapped to a single namespace URI, and a namespace URI is uniquely
  40. * mapped to a single namespace alias. In other words, it is not possible to have duplicates.
  41. * </p>
  42. *
  43. * <p>
  44. * Sample usage:
  45. * </p>
  46. *
  47. * <pre>{@code
  48. static final XmlNamespaceDictionary DICTIONARY = new XmlNamespaceDictionary()
  49. .set("", "http://www.w3.org/2005/Atom")
  50. .set("activity", "http://activitystrea.ms/spec/1.0/")
  51. .set("georss", "http://www.georss.org/georss")
  52. .set("media", "http://search.yahoo.com/mrss/")
  53. .set("thr", "http://purl.org/syndication/thread/1.0");
  54. *}</pre>
  55. *
  56. * @since 1.0
  57. * @author Yaniv Inbar
  58. */
  59. public final class XmlNamespaceDictionary {
  60. /**
  61. * Map from XML namespace alias (or {@code ""} for the default namespace) to XML namespace URI.
  62. */
  63. private final HashMap<String, String> namespaceAliasToUriMap = new HashMap<String, String>();
  64. /**
  65. * Map from XML namespace URI to XML namespace alias (or {@code ""} for the default namespace).
  66. */
  67. private final HashMap<String, String> namespaceUriToAliasMap = new HashMap<String, String>();
  68. /**
  69. * Returns the namespace alias (or {@code ""} for the default namespace) for the given namespace
  70. * URI.
  71. *
  72. * @param uri namespace URI
  73. * @since 1.3
  74. */
  75. public synchronized String getAliasForUri(String uri) {
  76. return namespaceUriToAliasMap.get(Preconditions.checkNotNull(uri));
  77. }
  78. /**
  79. * Returns the namespace URI for the given namespace alias (or {@code ""} for the default
  80. * namespace).
  81. *
  82. * @param alias namespace alias (or {@code ""} for the default namespace)
  83. * @since 1.3
  84. */
  85. public synchronized String getUriForAlias(String alias) {
  86. return namespaceAliasToUriMap.get(Preconditions.checkNotNull(alias));
  87. }
  88. /**
  89. * Returns an unmodified set of map entries for the map from namespace alias (or {@code ""} for
  90. * the default namespace) to namespace URI.
  91. *
  92. * @since 1.3
  93. */
  94. public synchronized Map<String, String> getAliasToUriMap() {
  95. return Collections.unmodifiableMap(namespaceAliasToUriMap);
  96. }
  97. /**
  98. * Returns an unmodified set of map entries for the map from namespace URI to namespace alias (or
  99. * {@code ""} for the default namespace).
  100. *
  101. * @since 1.3
  102. */
  103. public synchronized Map<String, String> getUriToAliasMap() {
  104. return Collections.unmodifiableMap(namespaceUriToAliasMap);
  105. }
  106. /**
  107. * Adds a namespace of the given alias and URI.
  108. *
  109. * <p>
  110. * If the uri is {@code null}, the namespace alias will be removed. Similarly, if the alias is
  111. * {@code null}, the namespace URI will be removed. Otherwise, if the alias is already mapped to a
  112. * different URI, it will be remapped to the new URI. Similarly, if a URI is already mapped to a
  113. * different alias, it will be remapped to the new alias.
  114. * </p>
  115. *
  116. * @param alias alias or {@code null} to remove the namespace URI
  117. * @param uri namespace URI or {@code null} to remove the namespace alias
  118. * @return this namespace dictionary
  119. * @since 1.3
  120. */
  121. public synchronized XmlNamespaceDictionary set(String alias, String uri) {
  122. String previousUri = null;
  123. String previousAlias = null;
  124. if (uri == null) {
  125. if (alias != null) {
  126. previousUri = namespaceAliasToUriMap.remove(alias);
  127. }
  128. } else if (alias == null) {
  129. previousAlias = namespaceUriToAliasMap.remove(uri);
  130. } else {
  131. previousUri = namespaceAliasToUriMap.put(
  132. Preconditions.checkNotNull(alias), Preconditions.checkNotNull(uri));
  133. if (!uri.equals(previousUri)) {
  134. previousAlias = namespaceUriToAliasMap.put(uri, alias);
  135. } else {
  136. previousUri = null;
  137. }
  138. }
  139. if (previousUri != null) {
  140. namespaceUriToAliasMap.remove(previousUri);
  141. }
  142. if (previousAlias != null) {
  143. namespaceAliasToUriMap.remove(previousAlias);
  144. }
  145. return this;
  146. }
  147. /**
  148. * Shows a debug string representation of an element data object of key/value pairs.
  149. *
  150. * @param element element data object ({@link GenericXml}, {@link Map}, or any object with public
  151. * fields)
  152. * @param elementName optional XML element local name prefixed by its namespace alias -- for
  153. * example {@code "atom:entry"} -- or {@code null} to make up something
  154. */
  155. public String toStringOf(String elementName, Object element) {
  156. try {
  157. StringWriter writer = new StringWriter();
  158. XmlSerializer serializer = Xml.createSerializer();
  159. serializer.setOutput(writer);
  160. serialize(serializer, elementName, element, false);
  161. return writer.toString();
  162. } catch (IOException e) {
  163. throw new IllegalArgumentException(e);
  164. }
  165. }
  166. /**
  167. * Shows a debug string representation of an element data object of key/value pairs.
  168. *
  169. * @param element element data object ({@link GenericXml}, {@link Map}, or any object with public
  170. * fields)
  171. * @param elementNamespaceUri XML namespace URI or {@code null} for no namespace
  172. * @param elementLocalName XML local name
  173. * @throws IOException I/O exception
  174. */
  175. public void serialize(
  176. XmlSerializer serializer, String elementNamespaceUri, String elementLocalName, Object element)
  177. throws IOException {
  178. serialize(serializer, elementNamespaceUri, elementLocalName, element, true);
  179. }
  180. /**
  181. * Shows a debug string representation of an element data object of key/value pairs.
  182. *
  183. * @param element element data object ({@link GenericXml}, {@link Map}, or any object with public
  184. * fields)
  185. * @param elementName XML element local name prefixed by its namespace alias
  186. * @throws IOException I/O exception
  187. */
  188. public void serialize(XmlSerializer serializer, String elementName, Object element)
  189. throws IOException {
  190. serialize(serializer, elementName, element, true);
  191. }
  192. private void serialize(XmlSerializer serializer, String elementNamespaceUri,
  193. String elementLocalName, Object element, boolean errorOnUnknown) throws IOException {
  194. String elementAlias = elementNamespaceUri == null ? null : getAliasForUri(elementNamespaceUri);
  195. startDoc(serializer, element, errorOnUnknown, elementAlias).serialize(
  196. serializer, elementNamespaceUri, elementLocalName);
  197. serializer.endDocument();
  198. }
  199. private void serialize(
  200. XmlSerializer serializer, String elementName, Object element, boolean errorOnUnknown)
  201. throws IOException {
  202. String elementAlias = "";
  203. if (elementName != null) {
  204. int colon = elementName.indexOf(':');
  205. if (colon != -1) {
  206. elementAlias = elementName.substring(0, colon);
  207. }
  208. }
  209. startDoc(serializer, element, errorOnUnknown, elementAlias).serialize(serializer, elementName);
  210. serializer.endDocument();
  211. }
  212. private ElementSerializer startDoc(
  213. XmlSerializer serializer, Object element, boolean errorOnUnknown, String elementAlias)
  214. throws IOException {
  215. serializer.startDocument(null, null);
  216. SortedSet<String> aliases = new TreeSet<String>();
  217. computeAliases(element, aliases);
  218. if (elementAlias != null) {
  219. aliases.add(elementAlias);
  220. }
  221. for (String alias : aliases) {
  222. String uri = getNamespaceUriForAliasHandlingUnknown(errorOnUnknown, alias);
  223. serializer.setPrefix(alias, uri);
  224. }
  225. return new ElementSerializer(element, errorOnUnknown);
  226. }
  227. private void computeAliases(Object element, SortedSet<String> aliases) {
  228. for (Map.Entry<String, Object> entry : Data.mapOf(element).entrySet()) {
  229. Object value = entry.getValue();
  230. if (value != null) {
  231. String name = entry.getKey();
  232. if (!Xml.TEXT_CONTENT.equals(name)) {
  233. int colon = name.indexOf(':');
  234. boolean isAttribute = name.charAt(0) == '@';
  235. if (colon != -1 || !isAttribute) {
  236. String alias = colon == -1 ? "" : name.substring(name.charAt(0) == '@' ? 1 : 0, colon);
  237. aliases.add(alias);
  238. }
  239. Class<?> valueClass = value.getClass();
  240. if (!isAttribute && !Data.isPrimitive(valueClass)) {
  241. if (value instanceof Iterable<?> || valueClass.isArray()) {
  242. for (Object subValue : Types.iterableOf(value)) {
  243. computeAliases(subValue, aliases);
  244. }
  245. } else {
  246. computeAliases(value, aliases);
  247. }
  248. }
  249. }
  250. }
  251. }
  252. }
  253. /**
  254. * Returns the namespace URI to use for serialization for a given namespace alias, possibly using
  255. * a predictable made-up namespace URI if the alias is not recognized.
  256. *
  257. * <p>
  258. * Specifically, if the namespace alias is not recognized, the namespace URI returned will be
  259. * {@code "http://unknown/"} plus the alias, unless {@code errorOnUnknown} is {@code true} in
  260. * which case it will throw an {@link IllegalArgumentException}.
  261. * </p>
  262. *
  263. * @param errorOnUnknown whether to thrown an exception if the namespace alias is not recognized
  264. * @param alias namespace alias
  265. * @return namespace URI, using a predictable made-up namespace URI if the namespace alias is not
  266. * recognized
  267. * @throws IllegalArgumentException if the namespace alias is not recognized and {@code
  268. * errorOnUnkown} is {@code true}
  269. */
  270. String getNamespaceUriForAliasHandlingUnknown(boolean errorOnUnknown, String alias) {
  271. String result = getUriForAlias(alias);
  272. if (result == null) {
  273. Preconditions.checkArgument(
  274. !errorOnUnknown, "unrecognized alias: %s", alias.length() == 0 ? "(default)" : alias);
  275. return "http://unknown/" + alias;
  276. }
  277. return result;
  278. }
  279. /**
  280. * Returns the namespace alias to use for a given namespace URI, throwing an exception if the
  281. * namespace URI can be found in this dictionary.
  282. *
  283. * @param namespaceUri namespace URI
  284. * @throws IllegalArgumentException if the namespace URI is not found in this dictionary
  285. */
  286. String getNamespaceAliasForUriErrorOnUnknown(String namespaceUri) {
  287. String result = getAliasForUri(namespaceUri);
  288. Preconditions.checkArgument(result != null,
  289. "invalid XML: no alias declared for namesapce <%s>; "
  290. + "work-around by setting XML namepace directly by calling the set method of %s",
  291. namespaceUri, XmlNamespaceDictionary.class.getName());
  292. return result;
  293. }
  294. class ElementSerializer {
  295. private final boolean errorOnUnknown;
  296. Object textValue = null;
  297. final List<String> attributeNames = new ArrayList<String>();
  298. final List<Object> attributeValues = new ArrayList<Object>();
  299. final List<String> subElementNames = new ArrayList<String>();
  300. final List<Object> subElementValues = new ArrayList<Object>();
  301. ElementSerializer(Object elementValue, boolean errorOnUnknown) {
  302. this.errorOnUnknown = errorOnUnknown;
  303. Class<?> valueClass = elementValue.getClass();
  304. if (Data.isPrimitive(valueClass) && !Data.isNull(elementValue)) {
  305. textValue = elementValue;
  306. } else {
  307. for (Map.Entry<String, Object> entry : Data.mapOf(elementValue).entrySet()) {
  308. Object fieldValue = entry.getValue();
  309. if (fieldValue != null && !Data.isNull(fieldValue)) {
  310. String fieldName = entry.getKey();
  311. if (Xml.TEXT_CONTENT.equals(fieldName)) {
  312. textValue = fieldValue;
  313. } else if (fieldName.charAt(0) == '@') {
  314. attributeNames.add(fieldName.substring(1));
  315. attributeValues.add(fieldValue);
  316. } else {
  317. subElementNames.add(fieldName);
  318. subElementValues.add(fieldValue);
  319. }
  320. }
  321. }
  322. }
  323. }
  324. void serialize(XmlSerializer serializer, String elementName) throws IOException {
  325. String elementLocalName = null;
  326. String elementNamespaceUri = null;
  327. if (elementName != null) {
  328. int colon = elementName.indexOf(':');
  329. elementLocalName = elementName.substring(colon + 1);
  330. String alias = colon == -1 ? "" : elementName.substring(0, colon);
  331. elementNamespaceUri = getNamespaceUriForAliasHandlingUnknown(errorOnUnknown, alias);
  332. }
  333. serialize(serializer, elementNamespaceUri, elementLocalName);
  334. }
  335. void serialize(XmlSerializer serializer, String elementNamespaceUri, String elementLocalName)
  336. throws IOException {
  337. boolean errorOnUnknown = this.errorOnUnknown;
  338. if (elementLocalName == null) {
  339. if (errorOnUnknown) {
  340. throw new IllegalArgumentException("XML name not specified");
  341. }
  342. elementLocalName = "unknownName";
  343. }
  344. serializer.startTag(elementNamespaceUri, elementLocalName);
  345. // attributes
  346. int num = attributeNames.size();
  347. for (int i = 0; i < num; i++) {
  348. String attributeName = attributeNames.get(i);
  349. int colon = attributeName.indexOf(':');
  350. String attributeLocalName = attributeName.substring(colon + 1);
  351. String attributeNamespaceUri = colon == -1 ? null : getNamespaceUriForAliasHandlingUnknown(
  352. errorOnUnknown, attributeName.substring(0, colon));
  353. serializer.attribute(
  354. attributeNamespaceUri, attributeLocalName, toSerializedValue(attributeValues.get(i)));
  355. }
  356. // text
  357. if (textValue != null) {
  358. serializer.text(toSerializedValue(textValue));
  359. }
  360. // elements
  361. num = subElementNames.size();
  362. for (int i = 0; i < num; i++) {
  363. Object subElementValue = subElementValues.get(i);
  364. String subElementName = subElementNames.get(i);
  365. Class<? extends Object> valueClass = subElementValue.getClass();
  366. if (subElementValue instanceof Iterable<?> || valueClass.isArray()) {
  367. for (Object subElement : Types.iterableOf(subElementValue)) {
  368. if (subElement != null && !Data.isNull(subElement)) {
  369. new ElementSerializer(subElement, errorOnUnknown).serialize(
  370. serializer, subElementName);
  371. }
  372. }
  373. } else {
  374. new ElementSerializer(subElementValue, errorOnUnknown).serialize(
  375. serializer, subElementName);
  376. }
  377. }
  378. serializer.endTag(elementNamespaceUri, elementLocalName);
  379. }
  380. }
  381. static String toSerializedValue(Object value) {
  382. if (value instanceof Float) {
  383. Float f = (Float) value;
  384. if (f.floatValue() == Float.POSITIVE_INFINITY) {
  385. return "INF";
  386. }
  387. if (f.floatValue() == Float.NEGATIVE_INFINITY) {
  388. return "-INF";
  389. }
  390. }
  391. if (value instanceof Double) {
  392. Double d = (Double) value;
  393. if (d.doubleValue() == Double.POSITIVE_INFINITY) {
  394. return "INF";
  395. }
  396. if (d.doubleValue() == Double.NEGATIVE_INFINITY) {
  397. return "-INF";
  398. }
  399. }
  400. if (value instanceof String || value instanceof Number || value instanceof Boolean) {
  401. return value.toString();
  402. }
  403. if (value instanceof DateTime) {
  404. return ((DateTime) value).toStringRfc3339();
  405. }
  406. if (value instanceof Enum<?>) {
  407. return FieldInfo.of((Enum<?>) value).getName();
  408. }
  409. throw new IllegalArgumentException("unrecognized value type: " + value.getClass());
  410. }
  411. }