/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.xml.stream;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Attribute;
import org.dom4j.Branch;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.tree.DefaultCDATA;
import org.projectforge.common.BeanHelper;
import org.projectforge.common.NumberHelper;
import org.projectforge.xml.stream.converter.IConverter;
/**
* Serializes objects to xml. A simple solution for streaming xml objects and to prevent default values from the xml output (because this
* feature isn't yet available in XStream). It's only fit the ProjectForge requirements and is not very useful as generic xml streaming
* package.
* @author Kai Reinhard (k.reinhard@micromata.de)
*
*/
public class XmlObjectWriter
{
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(XmlObjectWriter.class);
public static final String ATTR_ID = "o-id";
public static final String ATTR_REF_ID = "ref-id";
private boolean onlyAnnotatedFields;
private AliasMap aliasMap;
private int refIdCounter = 0;
private XmlRegistry xmlRegistry = XmlRegistry.baseRegistry();
/**
* Key is the class name with the hashCode of the object, e. g.: org.projectforge.xml.stream.TestObject:987345678854 and the value is the
* element where the object was written to.
*/
private Map<String, Element> writtenObjects = new HashMap<String, Element>();
/**
* For customization, the base xml registry of XmlRegistry is used at default.
* @param xmlRegistry
*/
public XmlObjectWriter setXmlRegistry(final XmlRegistry xmlRegistry)
{
this.xmlRegistry = xmlRegistry;
return this;
}
public XmlObjectWriter setAliasMap(final AliasMap aliasMap)
{
this.aliasMap = aliasMap;
return this;
}
private AliasMap getAliasMap()
{
if (this.aliasMap == null) {
this.aliasMap = new AliasMap();
}
return this.aliasMap;
}
/**
* Writes the given object as xml.
* @param obj
*/
public static String writeAsXml(final Object obj, final boolean prettyFormat)
{
return writeAsXml(obj, null, prettyFormat);
}
/**
* Writes the given object as xml.
* @param obj
*/
public static String writeAsXml(final Object obj)
{
return writeAsXml(obj, null, false);
}
/**
* Writes the given object as xml.
* @param obj
* @param aliasMap
*/
public static String writeAsXml(final Object obj, final AliasMap aliasMap)
{
return writeAsXml(obj, aliasMap, false);
}
/**
* Writes the given object as xml.
* @param obj
* @param aliasMap
* @param prettyFormat
*/
public static String writeAsXml(final Object obj, final AliasMap aliasMap, final boolean prettyFormat)
{
final XmlObjectWriter xmlWriter = new XmlObjectWriter();
if (aliasMap != null) {
xmlWriter.setAliasMap(aliasMap);
}
return xmlWriter.writeToXml(obj, prettyFormat);
}
/**
* Reader and Writer will ignore static and final fields.
* @return true if the field has to be ignored by the writer.
*/
public static boolean ignoreField(final Field field)
{
final int modifiers = field.getModifiers();
return Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers) == true || Modifier.isTransient(modifiers);
}
public XmlObjectWriter setOnlyAnnotatedFields(boolean onlyAnnotatedFields)
{
this.onlyAnnotatedFields = onlyAnnotatedFields;
return this;
}
public String writeToXml(final Object obj)
{
return writeToXml(obj, false);
}
public String writeToXml(final Object obj, final boolean prettyFormat)
{
final Document document = DocumentHelper.createDocument();
final Element element = write(document, obj);
return XmlHelper.toString(element, prettyFormat);
}
public Element write(final Branch parent, final Object obj)
{
reset();
return write(parent, obj, null, false, false);
}
private void reset()
{
this.writtenObjects.clear();
}
private Element write(final Branch parent, final Object obj, String name, final boolean asAttribute, final boolean asCDATA)
{
if (obj == null) {
return null;
}
final Class< ? > type = obj.getClass();
if (name == null) {
name = getAliasMap().getAliasForClass(type);
}
if (name == null && obj.getClass().isAnnotationPresent(XmlObject.class) == true) {
final XmlObject xmlObject = obj.getClass().getAnnotation(XmlObject.class);
if (StringUtils.isNotEmpty(xmlObject.alias()) == true) {
name = xmlObject.alias();
}
}
if (name == null) {
name = xmlRegistry.getAliasForClass(type);
}
if (name == null) {
name = obj.getClass().getName();
}
if (isRegistered(obj) == true) {
final Element el = getRegisteredElement(obj);
Integer refId;
final Attribute attr = el.attribute(ATTR_ID);
if (attr == null) {
// Id attribute not yet written. So add this attribute:
refId = refIdCounter++;
el.addAttribute(ATTR_ID, String.valueOf(refId));
} else {
refId = NumberHelper.parseInteger(attr.getText());
}
if (refId == null) {
log.error("Can't parse ref id: " + attr.getText());
} else {
final Element element = parent.addElement(name);
element.addAttribute(ATTR_REF_ID, String.valueOf(refId));
return element;
}
}
IConverter< ? > converter = xmlRegistry.getConverter(type);
if (converter != null) {
final String sValue = converter.toString(obj);
writeValue(parent, obj, name, sValue, asAttribute, asCDATA);
return (Element) parent;
} else if (Enum.class.isAssignableFrom(type) == true) {
final String sValue = ((Enum< ? >) obj).name();
writeValue(parent, obj, name, sValue, asAttribute, asCDATA);
return (Element) parent;
} else if (obj instanceof Collection< ? >) {
final Element listElement = parent.addElement(name);
final Iterator< ? > it = ((Collection< ? >) obj).iterator();
while (it.hasNext() == true) {
write(listElement, it.next(), null, false, false);
}
return listElement;
}
final Element element = parent.addElement(name);
registerElement(obj, element);
final Field[] fields = BeanHelper.getAllDeclaredFields(obj.getClass());
AccessibleObject.setAccessible(fields, true);
for (final Field field : fields) {
if (field.isAnnotationPresent(XmlOmitField.class) == true || ignoreField(obj, field) == true) {
continue;
}
final XmlField ann = field.isAnnotationPresent(XmlField.class) == true ? field.getAnnotation(XmlField.class) : null;
if (onlyAnnotatedFields == true && field.isAnnotationPresent(XmlField.class) == false) {
continue;
}
final Object value = BeanHelper.getFieldValue(obj, field);
writeField(field, obj, value, ann, element);
}
return element;
}
protected void writeField(final Field field, final Object obj, final Object fieldValue, final XmlField annotation, final Element element)
{
if (fieldValue == null || isDefaultType(annotation, fieldValue) == true) {
return;
}
boolean childAsAttribute = false;
if (annotation != null) {
if (annotation.asElement() == false && (asAttributeAsDefault(field.getType()) == true || annotation.asAttribute() == true)) {
childAsAttribute = true;
}
} else if (asAttributeAsDefault(field.getType()) == true) {
childAsAttribute = true;
}
final boolean childAsCDATA = (annotation != null && annotation.asCDATA() == true);
final String childName;
if (annotation != null && StringUtils.isNotEmpty(annotation.alias()) == true) {
childName = annotation.alias();
} else {
childName = field.getName();
}
write(element, fieldValue, childName, childAsAttribute, childAsCDATA);
}
protected void addAttribute(final Element element, final Object obj, final String name, final String value)
{
element.addAttribute(name, value);
}
private void writeValue(final Branch branch, final Object obj, final String key, final String sValue, final boolean asAttribute,
final boolean asCDATA)
{
if (sValue == null) {
return;
}
if (asAttribute == true) {
addAttribute((Element) branch, obj, key, sValue);
} else if (asCDATA == true) {
branch.addElement(key).add(new DefaultCDATA(sValue));
} else {
branch.addElement(key).setText(sValue);
}
}
private void registerElement(final Object obj, final Element element)
{
writtenObjects.put(obj.getClass().getName() + ":" + obj.hashCode(), element);
}
private Element getRegisteredElement(final Object obj)
{
return writtenObjects.get(obj.getClass().getName() + ":" + obj.hashCode());
}
private boolean isRegistered(final Object obj)
{
return writtenObjects.containsKey(obj.getClass().getName() + ":" + obj.hashCode());
}
/**
* Overload this method for further control. Don't forget to call super!
* @param obj
* @param field
* @return true if the field has to be ignored by the writer.
* @see #ignoreField(Field)
*/
protected boolean ignoreField(final Object obj, final Field field)
{
return ignoreField(field);
}
/**
* For some types (primitive types as double and int) serialization as attributes instead of elements is used.
* @param type
* @return true if for the given type a serialization as attribute should be used at default.
*/
public boolean asAttributeAsDefault(final Class< ? > type)
{
return xmlRegistry.asAttributeAsDefault(type) || Enum.class.isAssignableFrom(type) == true;
}
private static boolean isDefaultType(final XmlField ann, final Object value)
{
if (hasDefaultType(ann, value) == false) {
// No default value given in the annotation.
return false;
}
if (value instanceof Boolean) {
if (ann != null) {
return ann.defaultBooleanValue() == (Boolean) value;
} else {
return (Boolean) value == false;
}
} else if (value instanceof Double) {
return ((Double) value).compareTo(ann.defaultDoubleValue()) == 0;
} else if (value instanceof Integer) {
return ((Integer) value).compareTo(ann.defaultIntValue()) == 0;
} else if (value instanceof String) {
return ann.defaultStringValue().equals((String) value) == true;
} else if (Enum.class.isAssignableFrom(value.getClass()) == true) {
return ann.defaultStringValue().equals(((Enum< ? >) value).name()) == true;
}
return false;
}
/**
* @param ann
* @param value
* @return True if the annotation has the default type (meaning that no value is set in the annotation as default type).
*/
private static boolean hasDefaultType(final XmlField ann, final Object value)
{
if (value instanceof Boolean) {
// Boolean values have already default type false, if nothing else declared.
return true;
} else if (ann == null) {
return false;
} else if (value instanceof Double) {
return Double.isNaN(ann.defaultDoubleValue()) == false;
} else if (value instanceof Integer) {
return ann.defaultIntValue() != XmlConstants.MAGIC_INT_NUMBER;
} else if (value instanceof String || Enum.class.isAssignableFrom(value.getClass()) == true) {
return XmlConstants.MAGIC_STRING.equals(ann.defaultStringValue()) == false;
}
return false;
}
}