org.archive.crawler.restlet.XmlMarshaller Maven / Gradle / Ivy
The newest version!
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.crawler.restlet;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import org.apache.commons.lang.StringUtils;
import org.restlet.ext.xml.XmlWriter;
import org.xml.sax.SAXException;
/**
* XmlMarshaller can be used to write data structures as simple xml. See
* {@link #marshalDocument(Writer, String, Object)} for more information.
*
* @author nlevitt
*/
public class XmlMarshaller {
protected static XmlWriter getXmlWriter(Writer writer) {
XmlWriter xmlWriter = new XmlWriter(writer);
// https://webarchive.jira.com/browse/HER-1603?focusedCommentId=22558#action_22558
xmlWriter.setDataFormat(true);
xmlWriter.setIndentStep(2);
return xmlWriter;
}
/**
* Writes {@code content} as xml to {@code writer}. Recursively descends
* into Maps, using keys as tag names. Iterates over items in arrays and
* Iterables, using "value" as the tag name. Marshals simple object values
* with {@link #toString()}. The result looks something like this:
*
*
* {@literal
*
* simpleObjectValue1
*
* subvalue1
*
* item1Value
* item2Value
*
* subvalue3
*
*
* }
*
*
* @param writer
* output writer
* @param rootTag
* xml document root tag name
* @param content
* data structure to marshal
* @throws IOException
*/
public static void marshalDocument(Writer writer, String rootTag,
Object content) throws IOException {
XmlWriter xmlWriter = getXmlWriter(writer);
try {
xmlWriter.startDocument();
marshal(xmlWriter, rootTag, content);
xmlWriter.endDocument(); // calls flush()
} catch (SAXException e) {
if (e.getException() instanceof IOException) {
// e.g. broken tcp connection
throw (IOException) e.getException();
} else {
throw new RuntimeException(e);
}
}
}
/**
* sort PropertyDescriptors according to propOrder.
* properties listed in propOrder come first, in the order they are listed, and
* then come remaining unlisted properties, in arbitrary order (likely alphabetical).
* this semantics is rather relaxed compared to original JAXB semantics, where props
* and propOrder must be the same set (except for those marked XmlTransient).
* @param props PropertyDescriptor array to be sorted in-place.
* @param propOrder list of property names in order of desired appearance.
*/
protected static void orderProperties(PropertyDescriptor[] props, final String[] propOrder) {
if (propOrder == null || propOrder.length == 0) return;
final Map order = new HashMap();
for (int i = 0; i < propOrder.length; i++) {
order.put(propOrder[i], i);
}
final Integer LAST = Integer.valueOf(propOrder.length);
Arrays.sort(props, new Comparator() {
@Override
public int compare(PropertyDescriptor o1, PropertyDescriptor o2) {
Integer c1 = order.get(o1.getName());
Integer c2 = order.get(o2.getName());
return (c1 != null ? c1 : LAST).compareTo(c2 != null ? c2 : LAST);
}
});
}
/**
* test if {@code obj} has {@link XmlRootElement} annotation.
* to avoid unexpected side effects, objects are mapped to nested XML structure
* only when its class has {@link XmlRootElement} annotation.
* note semantics is slightly different from JAXB - just borrowing XmlRootElement
* as substitute of XmlElement, because Map entry cannot be annotated.
* @param obj object to test
* @return true if obj's class has XmlRootElement annotation.
*/
protected static boolean marshalAsElement(Object obj) {
XmlRootElement ann = obj.getClass().getAnnotation(XmlRootElement.class);
return ann != null;
}
/**
* generate nested XML structure for a bean {@code obj}. enclosing element
* will not be generated if {@code key} is empty. each readable JavaBeans property
* is mapped to an nested element named after its name. Those properties
* annotated with {@link XmlTransient} are ignored.
* @param xmlWriter XmlWriter
* @param key name of enclosing element
* @param obj bean
* @throws SAXException
*/
protected static void marshalBean(XmlWriter xmlWriter, String key, Object obj) throws SAXException {
if (!StringUtils.isEmpty(key))
xmlWriter.startElement(key);
try {
BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass(), Object.class);
PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
XmlType xmlType = obj.getClass().getAnnotation(XmlType.class);
if (xmlType != null) {
String[] propOrder = xmlType.propOrder();
if (propOrder != null) {
// TODO: should cache this sorted version?
orderProperties(props, propOrder);
}
}
for (PropertyDescriptor prop : props) {
Method m = prop.getReadMethod();
if (m == null || m.getAnnotation(XmlTransient.class) != null)
continue;
try {
Object propValue = m.invoke(obj);
if (propValue != null && !"".equals(propValue)) {
marshal(xmlWriter, prop.getName(), propValue);
}
} catch (Exception ex) {
// generate empty element, for now. generate comment?
xmlWriter.emptyElement(prop.getName());
}
}
} catch (IntrospectionException ex) {
// ignored, for now.
}
if (!StringUtils.isEmpty(key))
xmlWriter.endElement(key);
}
protected static void marshal(XmlWriter xmlWriter, String key, Object value) throws SAXException {
if (value == null) {
xmlWriter.emptyElement(key);
} else if (value instanceof Map,?>) {
marshal(xmlWriter, key, (Map,?>) value);
} else if (value instanceof Iterable>) {
marshal(xmlWriter, key, (Iterable>) value);
} else if (marshalAsElement(value)) {
marshalBean(xmlWriter, key, value);
} else {
xmlWriter.dataElement(key, value.toString());
}
}
protected static void marshal(XmlWriter xmlWriter, String key, Map,?> map) throws SAXException {
xmlWriter.startElement(key);
for (Map.Entry,?> entry: map.entrySet()) {
marshal(xmlWriter, entry.getKey().toString(), entry.getValue());
}
xmlWriter.endElement(key);
}
protected static void marshal(XmlWriter xmlWriter, String key, Iterable> iterable) throws SAXException {
xmlWriter.startElement(key);
for (Object item: iterable) {
marshal(xmlWriter, item);
}
xmlWriter.endElement(key);
}
// something we have no name for goes in a tag
protected static void marshal(XmlWriter xmlWriter, Object item) throws SAXException {
if (item instanceof Map.Entry, ?>) {
// if it happens to have a name, use it
Map.Entry, ?> entry = (Map.Entry, ?>) item;
marshal(xmlWriter, entry.getKey().toString(), entry.getValue());
} else {
marshal(xmlWriter, "value", item);
}
}
}