com.google.api.client.xml.Xml Maven / Gradle / Ivy
Show all versions of google-api-client Show documentation
package com.google.api.client.xml;
import com.google.api.client.util.ClassInfo;
import com.google.api.client.util.FieldInfo;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
/**
* XML utilities.
*
* @since 1.0
* @author Yaniv Inbar
*/
public class Xml {
/** XML Parser factory. */
public static XmlParserFactory parserFactory;
/** Returns the parser factory. */
private static XmlParserFactory getParserFactory()
throws XmlPullParserException {
XmlParserFactory parserFactory = Xml.parserFactory;
if (parserFactory == null) {
parserFactory = Xml.parserFactory = DefaultXmlParserFactory.getInstance();
}
return parserFactory;
}
/**
* Returns a new XML serializer.
*
* @throws IllegalArgumentException if encountered an
* {@link XmlPullParserException}
*/
public static XmlSerializer createSerializer() {
try {
return getParserFactory().createSerializer();
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(e);
}
}
/** Returns a new XML pull parser. */
public static XmlPullParser createParser() throws XmlPullParserException {
XmlPullParser result = getParserFactory().createParser();
if (!result.getFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES)) {
throw new IllegalStateException(
"XML pull parser must have " + "namespace-aware feature");
}
// TODO: make the XML pull parser secure
return result;
}
/**
* 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);
}
private static void parseValue(String stringValue, Field field,
Object destination, GenericXml genericXml,
Map destinationMap, String name) {
if (field == null) {
if (genericXml != null) {
genericXml.set(name, parseValue(stringValue, null));
} else if (destinationMap != null) {
destinationMap.put(name, parseValue(stringValue, null));
}
} else {
Class> fieldClass = field.getType();
if (Modifier.isFinal(field.getModifiers())
&& !FieldInfo.isPrimitive(fieldClass)) {
throw new IllegalArgumentException(
"final sub-element fields are not supported");
}
Object fieldValue = parseValue(stringValue, fieldClass);
FieldInfo.setFieldValue(field, destination, fieldValue);
}
}
/**
* Customizes the behavior of XML parsing. Subclasses may override any methods
* they need to customize behavior.
*/
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 elment using the given XML pull parser into the given
* destination object.
*
* Requires the 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 {
parseElementInternal(
parser, destination, 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,
Object destination, XmlNamespaceDictionary namespaceDictionary,
CustomizeParser customizeParser)
throws IOException, XmlPullParserException {
Class> destinationClass =
destination == null ? null : destination.getClass();
GenericXml genericXml =
destination instanceof GenericXml ? (GenericXml) destination : null;
boolean isMap = genericXml == null && destination instanceof Map, ?>;
@SuppressWarnings("unchecked")
Map destinationMap =
isMap ? (Map) destination : null;
ClassInfo classInfo =
isMap || destination == null ? null : ClassInfo.of(destinationClass);
int eventType = parser.getEventType();
if (parser.getEventType() == XmlPullParser.START_DOCUMENT) {
eventType = parser.next();
}
if (eventType != XmlPullParser.START_TAG) {
throw new IllegalArgumentException(
"expected start of XML element, but got something else (event type "
+ eventType + ")");
}
// generic XML
String prefix = parser.getPrefix();
String alias = prefix == null ? "" : prefix;
namespaceDictionary.addNamespace(alias, parser.getNamespace());
// TODO: can instead just look at the xmlns attributes?
if (genericXml != null) {
genericXml.namespaceDictionary = namespaceDictionary;
String name = parser.getName();
genericXml.name = prefix == null ? name : prefix + ":" + name;
}
// attributes
if (destination != null) {
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String attributeName = parser.getAttributeName(i);
String attributePrefix = parser.getAttributePrefix(i);
String attributeNamespace = parser.getAttributeNamespace(i);
if (attributePrefix != null) {
namespaceDictionary.addNamespace(attributePrefix, attributeNamespace);
}
String fieldName = getFieldName(
true, attributePrefix, attributeNamespace, attributeName);
Field field = isMap ? null : classInfo.getField(fieldName);
parseValue(parser.getAttributeValue(i), field, destination, genericXml,
destinationMap, fieldName);
}
}
Field field;
while (true) {
int event = parser.next();
switch (event) {
case XmlPullParser.END_DOCUMENT:
return true;
case XmlPullParser.END_TAG:
return customizeParser != null && customizeParser.stopAfterEndTag(
parser.getNamespace(), parser.getName());
case XmlPullParser.TEXT:
// parse text content
if (destination != null) {
String textFieldName = "text()";
field = isMap ? null : classInfo.getField(textFieldName);
parseValue(parser.getText(), field, destination, genericXml,
destinationMap, textFieldName);
}
break;
case XmlPullParser.START_TAG:
if (customizeParser != null && customizeParser.stopBeforeStartTag(
parser.getNamespace(), parser.getName())) {
return true;
}
if (destination == null) {
int level = 1;
while (level != 0) {
switch (parser.next()) {
case XmlPullParser.END_DOCUMENT:
return true;
case XmlPullParser.START_TAG:
level++;
break;
case XmlPullParser.END_TAG:
level--;
break;
}
}
continue;
}
// element
String fieldName =
getFieldName(false, parser.getPrefix(), parser.getNamespace(),
parser.getName());
field = isMap ? null : classInfo.getField(fieldName);
Class> fieldClass = field == null ? null : field.getType();
boolean isStopped = false;
if (field == null && !isMap && genericXml == null || field != null
&& FieldInfo.isPrimitive(fieldClass)) {
int level = 1;
while (level != 0) {
switch (parser.next()) {
case XmlPullParser.END_DOCUMENT:
return true;
case XmlPullParser.START_TAG:
level++;
break;
case XmlPullParser.END_TAG:
level--;
break;
case XmlPullParser.TEXT:
if (level == 1) {
parseValue(parser.getText(), field, destination, genericXml,
destinationMap, fieldName);
}
break;
}
}
} else if (field == null || Map.class.isAssignableFrom(fieldClass)) {
// TODO: handle sub-field type
Map mapValue = ClassInfo.newMapInstance(fieldClass);
isStopped = parseElementInternal(
parser, mapValue, namespaceDictionary, customizeParser);
if (isMap) {
@SuppressWarnings("unchecked")
Collection