All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.api.client.xml.Xml Maven / Gradle / Ivy

The newest version!
/*
 * 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.http.HttpMediaType;
import com.google.api.client.util.ArrayValueMap;
import com.google.api.client.util.Beta;
import com.google.api.client.util.Charsets;
import com.google.api.client.util.ClassInfo;
import com.google.api.client.util.Data;
import com.google.api.client.util.FieldInfo;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.Types;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;

/**
 * {@link Beta} 
* XML utilities. * * @since 1.0 * @author Yaniv Inbar */ @Beta public class Xml { /** * {@code "application/xml; charset=utf-8"} media type used as a default for XML parsing. * *

Use {@link HttpMediaType#equalsIgnoreParameters} for comparing media types. * * @since 1.10 */ public static final String MEDIA_TYPE = new HttpMediaType("application/xml").setCharsetParameter(Charsets.UTF_8).build(); /** Text content. */ static final String TEXT_CONTENT = "text()"; /** XML pull parser factory. */ private static XmlPullParserFactory factory; private static synchronized XmlPullParserFactory getParserFactory() throws XmlPullParserException { if (factory == null) { factory = XmlPullParserFactory.newInstance( System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); factory.setNamespaceAware(true); } return factory; } /** * Returns a new XML serializer. * * @throws IllegalArgumentException if encountered an {@link XmlPullParserException} */ public static XmlSerializer createSerializer() { try { return getParserFactory().newSerializer(); } catch (XmlPullParserException e) { throw new IllegalArgumentException(e); } } /** Returns a new XML pull parser. */ public static XmlPullParser createParser() throws XmlPullParserException { return getParserFactory().newPullParser(); } /** * Shows a debug string representation of an element data object of key/value pairs. * *

It will make up something for the element name and XML namespaces. If those are known, it is * better to use {@link XmlNamespaceDictionary#toStringOf(String, Object)}. * * @param element element data object of key/value pairs ({@link GenericXml}, {@link Map}, or any * object with public fields) */ public static String toStringOf(Object element) { return new XmlNamespaceDictionary().toStringOf(null, element); } /** * Parses the string value of an attribute value or text content. * * @param stringValue string value * @param field field to set or {@code null} if not applicable * @param valueType value type (class, parameterized type, or generic array type) or {@code null} * for none * @param context context list, going from least specific to most specific type context, for * example container class and its field * @param destination destination object or {@code null} for none * @param genericXml generic XML or {@code null} if not applicable * @param destinationMap destination map or {@code null} if not applicable * @param name key name */ private static void parseAttributeOrTextContent( String stringValue, Field field, Type valueType, List context, Object destination, GenericXml genericXml, Map destinationMap, String name) { if (field != null || genericXml != null || destinationMap != null) { valueType = field == null ? valueType : field.getGenericType(); Object value = parseValue(valueType, context, stringValue); setValue(value, field, destination, genericXml, destinationMap, name); } } /** * Sets the value of a given field or map entry. * * @param value value * @param field field to set or {@code null} if not applicable * @param destination destination object or {@code null} for none * @param genericXml generic XML or {@code null} if not applicable * @param destinationMap destination map or {@code null} if not applicable * @param name key name */ private static void setValue( Object value, Field field, Object destination, GenericXml genericXml, Map destinationMap, String name) { if (field != null) { FieldInfo.setFieldValue(field, destination, value); } else if (genericXml != null) { genericXml.set(name, value); } else { destinationMap.put(name, value); } } /** * Customizes the behavior of XML parsing. Subclasses may override any methods they need to * customize behavior. * *

Implementation has no fields and therefore thread-safe, but sub-classes are not necessarily * thread-safe. */ public static class CustomizeParser { /** * Returns whether to stop parsing when reaching the start tag of an XML element before it has * been processed. Only called if the element is actually being processed. By default, returns * {@code false}, but subclasses may override. * * @param namespace XML element's namespace URI * @param localName XML element's local name */ public boolean stopBeforeStartTag(String namespace, String localName) { return false; } /** * Returns whether to stop parsing when reaching the end tag of an XML element after it has been * processed. Only called if the element is actually being processed. By default, returns {@code * false}, but subclasses may override. * * @param namespace XML element's namespace URI * @param localName XML element's local name */ public boolean stopAfterEndTag(String namespace, String localName) { return false; } } /** * Parses an XML element using the given XML pull parser into the given destination object. * *

Requires the current event be {@link XmlPullParser#START_TAG} (skipping any initial {@link * XmlPullParser#START_DOCUMENT}) of the element being parsed. At normal parsing completion, the * current event will either be {@link XmlPullParser#END_TAG} of the element being parsed, or the * {@link XmlPullParser#START_TAG} of the requested {@code atom:entry}. * * @param parser XML pull parser * @param destination optional destination object to parser into or {@code null} to ignore XML * content * @param namespaceDictionary XML namespace dictionary to store unknown namespaces * @param customizeParser optional parser customizer or {@code null} for none */ public static void parseElement( XmlPullParser parser, Object destination, XmlNamespaceDictionary namespaceDictionary, CustomizeParser customizeParser) throws IOException, XmlPullParserException { ArrayList context = new ArrayList(); if (destination != null) { context.add(destination.getClass()); } parseElementInternal(parser, context, destination, null, namespaceDictionary, customizeParser); } /** * Returns whether the customize parser has requested to stop or reached end of document. * Otherwise, identical to {@link #parseElement(XmlPullParser, Object, XmlNamespaceDictionary, * CustomizeParser)} . */ private static boolean parseElementInternal( XmlPullParser parser, ArrayList context, Object destination, Type valueType, XmlNamespaceDictionary namespaceDictionary, CustomizeParser customizeParser) throws IOException, XmlPullParserException { // TODO(yanivi): method is too long; needs to be broken down into smaller methods and comment // better GenericXml genericXml = destination instanceof GenericXml ? (GenericXml) destination : null; @SuppressWarnings("unchecked") Map destinationMap = genericXml == null && destination instanceof Map ? Map.class.cast(destination) : null; // if there is a class, we want to put the data into, create the class Info for this ClassInfo classInfo = destinationMap != null || destination == null ? null : ClassInfo.of(destination.getClass()); if (parser.getEventType() == XmlPullParser.START_DOCUMENT) { parser.next(); } parseNamespacesForElement(parser, namespaceDictionary); // generic XML if (genericXml != null) { genericXml.namespaceDictionary = namespaceDictionary; String name = parser.getName(); String namespace = parser.getNamespace(); String alias = namespaceDictionary.getNamespaceAliasForUriErrorOnUnknown(namespace); genericXml.name = alias.length() == 0 ? name : alias + ":" + name; } // attributes if (destination != null) { int attributeCount = parser.getAttributeCount(); for (int i = 0; i < attributeCount; i++) { // TODO(yanivi): can have repeating attribute values, e.g. "@a=value1 @a=value2"? String attributeName = parser.getAttributeName(i); String attributeNamespace = parser.getAttributeNamespace(i); String attributeAlias = attributeNamespace.length() == 0 ? "" : namespaceDictionary.getNamespaceAliasForUriErrorOnUnknown(attributeNamespace); String fieldName = getFieldName(true, attributeAlias, attributeNamespace, attributeName); Field field = classInfo == null ? null : classInfo.getField(fieldName); parseAttributeOrTextContent( parser.getAttributeValue(i), field, valueType, context, destination, genericXml, destinationMap, fieldName); } } Field field; ArrayValueMap arrayValueMap = new ArrayValueMap(destination); boolean isStopped = false; // TODO(yanivi): support Void type as "ignore" element/attribute main: while (true) { int event = parser.next(); switch (event) { case XmlPullParser.END_DOCUMENT: isStopped = true; break main; case XmlPullParser.END_TAG: isStopped = customizeParser != null && customizeParser.stopAfterEndTag(parser.getNamespace(), parser.getName()); break main; case XmlPullParser.TEXT: // parse text content if (destination != null) { field = classInfo == null ? null : classInfo.getField(TEXT_CONTENT); parseAttributeOrTextContent( parser.getText(), field, valueType, context, destination, genericXml, destinationMap, TEXT_CONTENT); } break; case XmlPullParser.START_TAG: if (customizeParser != null && customizeParser.stopBeforeStartTag(parser.getNamespace(), parser.getName())) { isStopped = true; break main; } if (destination == null) { parseTextContentForElement(parser, context, true, null); } else { // element parseNamespacesForElement(parser, namespaceDictionary); String namespace = parser.getNamespace(); String alias = namespaceDictionary.getNamespaceAliasForUriErrorOnUnknown(namespace); // get the "real" field name of the String fieldName = getFieldName(false, alias, namespace, parser.getName()); // fetch the field from the classInfo field = classInfo == null ? null : classInfo.getField(fieldName); Type fieldType = field == null ? valueType : field.getGenericType(); fieldType = Data.resolveWildcardTypeOrTypeVariable(context, fieldType); // field type is now class, parameterized type, or generic array type // resolve a parameterized type to a class Class fieldClass = fieldType instanceof Class ? (Class) fieldType : null; if (fieldType instanceof ParameterizedType) { fieldClass = Types.getRawClass((ParameterizedType) fieldType); } boolean isArray = Types.isArray(fieldType); // text content boolean ignore = field == null && destinationMap == null && genericXml == null; // is the field an Enum boolean isEnum = fieldClass != null && fieldClass.isEnum(); if (ignore || Data.isPrimitive(fieldType) || isEnum) { int level = 1; while (level != 0) { switch (parser.next()) { case XmlPullParser.END_DOCUMENT: isStopped = true; break main; case XmlPullParser.START_TAG: level++; break; case XmlPullParser.END_TAG: level--; break; case XmlPullParser.TEXT: if (!ignore && level == 1) { parseAttributeOrTextContent( parser.getText(), field, valueType, context, destination, genericXml, destinationMap, fieldName); } break; default: break; } } } else if (fieldType == null || fieldClass != null && Types.isAssignableToOrFrom(fieldClass, Map.class)) { // store the element as a map Map mapValue = Data.newMapInstance(fieldClass); int contextSize = context.size(); if (fieldType != null) { context.add(fieldType); } Type subValueType = fieldType != null && Map.class.isAssignableFrom(fieldClass) ? Types.getMapValueParameter(fieldType) : null; subValueType = Data.resolveWildcardTypeOrTypeVariable(context, subValueType); isStopped = parseElementInternal( parser, context, mapValue, subValueType, namespaceDictionary, customizeParser); if (fieldType != null) { context.remove(contextSize); } if (destinationMap != null) { // map but not GenericXml: store as ArrayList of elements @SuppressWarnings("unchecked") Collection list = (Collection) destinationMap.get(fieldName); if (list == null) { list = new ArrayList(1); destinationMap.put(fieldName, list); } list.add(mapValue); } else if (field != null) { // not a map: store in field value FieldInfo fieldInfo = FieldInfo.of(field); if (fieldClass == Object.class) { // field is an Object: store as ArrayList of element maps @SuppressWarnings("unchecked") Collection list = (Collection) fieldInfo.getValue(destination); if (list == null) { list = new ArrayList(1); fieldInfo.setValue(destination, list); } list.add(mapValue); } else { // field is a Map: store as a single element map fieldInfo.setValue(destination, mapValue); } } else { // GenericXml: store as ArrayList of elements GenericXml atom = (GenericXml) destination; @SuppressWarnings("unchecked") Collection list = (Collection) atom.get(fieldName); if (list == null) { list = new ArrayList(1); atom.set(fieldName, list); } list.add(mapValue); } } else if (isArray || Types.isAssignableToOrFrom(fieldClass, Collection.class)) { // TODO(yanivi): some duplicate code here; isolate into reusable methods FieldInfo fieldInfo = FieldInfo.of(field); Object elementValue = null; Type subFieldType = isArray ? Types.getArrayComponentType(fieldType) : Types.getIterableParameter(fieldType); Class rawArrayComponentType = Types.getRawArrayComponentType(context, subFieldType); subFieldType = Data.resolveWildcardTypeOrTypeVariable(context, subFieldType); Class subFieldClass = subFieldType instanceof Class ? (Class) subFieldType : null; if (subFieldType instanceof ParameterizedType) { subFieldClass = Types.getRawClass((ParameterizedType) subFieldType); } boolean isSubEnum = subFieldClass != null && subFieldClass.isEnum(); if (Data.isPrimitive(subFieldType) || isSubEnum) { elementValue = parseTextContentForElement(parser, context, false, subFieldType); } else if (subFieldType == null || subFieldClass != null && Types.isAssignableToOrFrom(subFieldClass, Map.class)) { elementValue = Data.newMapInstance(subFieldClass); int contextSize = context.size(); if (subFieldType != null) { context.add(subFieldType); } Type subValueType = subFieldType != null && Map.class.isAssignableFrom(subFieldClass) ? Types.getMapValueParameter(subFieldType) : null; subValueType = Data.resolveWildcardTypeOrTypeVariable(context, subValueType); isStopped = parseElementInternal( parser, context, elementValue, subValueType, namespaceDictionary, customizeParser); if (subFieldType != null) { context.remove(contextSize); } } else { elementValue = Types.newInstance(rawArrayComponentType); int contextSize = context.size(); context.add(fieldType); isStopped = parseElementInternal( parser, context, elementValue, null, namespaceDictionary, customizeParser); context.remove(contextSize); } if (isArray) { // array field: add new element to array value map if (field == null) { arrayValueMap.put(fieldName, rawArrayComponentType, elementValue); } else { arrayValueMap.put(field, rawArrayComponentType, elementValue); } } else { // collection: add new element to collection @SuppressWarnings("unchecked") Collection collectionValue = (Collection) (field == null ? destinationMap.get(fieldName) : fieldInfo.getValue(destination)); if (collectionValue == null) { collectionValue = Data.newCollectionInstance(fieldType); setValue( collectionValue, field, destination, genericXml, destinationMap, fieldName); } collectionValue.add(elementValue); } } else { // not an array/iterable or a map, but we do have a field Object value = Types.newInstance(fieldClass); int contextSize = context.size(); context.add(fieldType); isStopped = parseElementInternal( parser, context, value, null, namespaceDictionary, customizeParser); context.remove(contextSize); setValue(value, field, destination, genericXml, destinationMap, fieldName); } } if (isStopped || parser.getEventType() == XmlPullParser.END_DOCUMENT) { isStopped = true; break main; } break; // break Switch; } // end -- switch (event) } // end -- main: while (true) arrayValueMap.setValues(); return isStopped; } private static String getFieldName( boolean isAttribute, String alias, String namespace, String name) { if (!isAttribute && alias.length() == 0) { return name; } StringBuilder buf = new StringBuilder(2 + alias.length() + name.length()); if (isAttribute) { buf.append('@'); } if (alias.length() != 0) { buf.append(alias).append(':'); } return buf.append(name).toString(); } private static Object parseTextContentForElement( XmlPullParser parser, List context, boolean ignoreTextContent, Type textContentType) throws XmlPullParserException, IOException { Object result = null; int level = 1; while (level != 0) { switch (parser.next()) { case XmlPullParser.END_DOCUMENT: level = 0; break; case XmlPullParser.START_TAG: level++; break; case XmlPullParser.END_TAG: level--; break; case XmlPullParser.TEXT: if (!ignoreTextContent && level == 1) { result = parseValue(textContentType, context, parser.getText()); } break; default: break; } } return result; } private static Object parseValue(Type valueType, List context, String value) { valueType = Data.resolveWildcardTypeOrTypeVariable(context, valueType); if (valueType == Double.class || valueType == double.class) { if (value.equals("INF")) { return Double.valueOf(Double.POSITIVE_INFINITY); } if (value.equals("-INF")) { return Double.valueOf(Double.NEGATIVE_INFINITY); } } if (valueType == Float.class || valueType == float.class) { if (value.equals("INF")) { return Float.POSITIVE_INFINITY; } if (value.equals("-INF")) { return Float.NEGATIVE_INFINITY; } } return Data.parsePrimitiveValue(valueType, value); } /** * Parses the namespaces declared on the current element into the namespace dictionary. * * @param parser XML pull parser * @param namespaceDictionary namespace dictionary */ private static void parseNamespacesForElement( XmlPullParser parser, XmlNamespaceDictionary namespaceDictionary) throws XmlPullParserException { int eventType = parser.getEventType(); Preconditions.checkState( eventType == XmlPullParser.START_TAG, "expected start of XML element, but got something else (event type %s)", eventType); int depth = parser.getDepth(); int nsStart = parser.getNamespaceCount(depth - 1); int nsEnd = parser.getNamespaceCount(depth); for (int i = nsStart; i < nsEnd; i++) { String namespace = parser.getNamespaceUri(i); // if namespace isn't already in our dictionary, add it now if (namespaceDictionary.getAliasForUri(namespace) == null) { String prefix = parser.getNamespacePrefix(i); String originalAlias = prefix == null ? "" : prefix; // find an available alias String alias = originalAlias; int suffix = 1; while (namespaceDictionary.getUriForAlias(alias) != null) { suffix++; alias = originalAlias + suffix; } namespaceDictionary.set(alias, namespace); } } } private Xml() {} }