Java tutorial
///////////////////////////////////////////////////////////////////////////// // // 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.framework.xstream; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.dom4j.Attribute; import org.dom4j.Element; import org.projectforge.common.BeanHelper; import org.projectforge.common.StringHelper; import org.projectforge.framework.xstream.converter.IConverter; /** * Parses objects serialized by the XmlObjectWriter. Uses the dom4j. * @author Kai Reinhard (k.reinhard@micromata.de) * */ public class XmlObjectReader { private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(XmlObjectReader.class); private static final int ABBREVIATE_WARNING = 500; private AliasMap aliasMap; private Map<Class<?>, Class<?>> implementationMapping; private XmlRegistry xmlRegistry = XmlRegistry.baseRegistry(); private String warnings; /** * This set is used for detecting ignored elements. */ private Set<Element> processedElements; /** * The values of the enclosed set are the name of the processed attributes. This map is used for detecting ignored attributes. */ private Map<Element, Set<String>> processedAttributes; /** * Key is the object id (o-id) and value is the already deserialized object. */ private final Map<String, Object> referenceObjects = new HashMap<String, Object>(); private boolean ignoreEmptyCollections; /** * For customization, the base xml registry of XmlRegistry is used at default. * @param xmlRegistry */ public void setXmlRegistry(final XmlRegistry xmlRegistry) { this.xmlRegistry = xmlRegistry; } public XmlObjectReader setAliasMap(final AliasMap aliasMap) { this.aliasMap = aliasMap; return this; } public XmlObjectReader addImplementationMapping(final Class<?> clazz, final Class<?> implementationClass) { if (this.implementationMapping == null) { this.implementationMapping = new HashMap<Class<?>, Class<?>>(); } this.implementationMapping.put(clazz, implementationClass); return this; } public Class<?> getImplemenationMapping(final Class<?> clazz) { if (this.implementationMapping == null) { return null; } else { return this.implementationMapping.get(clazz); } } /** * If true then empty collections of the xml will not be initialized. The corresponding field is left null instead of creating a empty * collection. * @param ignoreEmptyCollections */ public void setIgnoreEmptyCollections(final boolean ignoreEmptyCollections) { this.ignoreEmptyCollections = ignoreEmptyCollections; } private AliasMap getAliasMap() { if (this.aliasMap == null) { this.aliasMap = new AliasMap(); } return this.aliasMap; } /** * Could be called multiple times. Parses the given object and all field recursively for annotations from type {@link XmlObject}. */ public void initialize(final Class<?> clazz) { initialize(new HashSet<Class<?>>(), clazz); } private void initialize(final Set<Class<?>> processed, final Class<?> clazz) { if (processed.contains(clazz) == true) { // Already processed, avoid endless loops: return; } processed.add(clazz); if (clazz.isAnnotationPresent(XmlObject.class) == true) { final XmlObject xmlObject = clazz.getAnnotation(XmlObject.class); if (StringUtils.isNotEmpty(xmlObject.alias()) == true) { getAliasMap().put(clazz, xmlObject.alias()); } } for (final Field field : BeanHelper.getAllDeclaredFields(clazz)) { if (field.getType().isPrimitive() == false && XmlObjectWriter.ignoreField(field) == false) { initialize(processed, field.getType()); } } } /** * @return Any warning messages produced during the xml reading. */ public String getWarnings() { return warnings; } public Object read(final String xml) { final Element element = XmlHelper.fromString(xml); processedElements = new HashSet<Element>(); processedAttributes = new HashMap<Element, Set<String>>(); warnings = null; final Object obj = read(element); warnings = checkForIgnoredElements(element); if (warnings != null) { if (warnings.length() > ABBREVIATE_WARNING) { log.warn("Warnings while parsing xml:\n" + StringUtils.abbreviate(warnings, ABBREVIATE_WARNING) + " (message abbreviated)."); } else { log.warn("Warnings while parsing xml:\n" + warnings); } } return obj; } public Object read(final Element el) { if (el == null) { return null; } final Class<?> clazz = getClass(el.getName()); if (clazz != null) { final Object obj = read(clazz, el, null, null); return obj; } else { return null; } } /** * @param el * @return Warning messages if exists, otherwise null. */ public String checkForIgnoredElements(final Element el) { if (el == null) { return null; } if (processedElements == null || processedAttributes == null) { return "No information available"; } final StringBuffer buf = new StringBuffer(); checkForIgnoredElements(buf, el); final String warnings = buf.toString(); if (StringUtils.isEmpty(warnings) == true) { return null; } return warnings; } private void checkForIgnoredElements(final StringBuffer buf, final Element el) { if (processedElements.contains(el) == false) { buf.append("Ignored xml element: ").append(el.getPath()).append("\n"); } @SuppressWarnings("rawtypes") final List attributes = el.attributes(); if (attributes != null) { final Set<String> attributeSet = processedAttributes.get(el); for (final Object attr : attributes) { if (attributeSet == null || attributeSet.contains(((Attribute) attr).getName()) == false) { buf.append("Ignored xml attribute: ").append(((Attribute) attr).getPath()).append("\n"); } } } @SuppressWarnings("rawtypes") final List children = el.elements(); if (children != null) { for (final Object child : children) { checkForIgnoredElements(buf, (Element) child); } } } protected Class<?> getClass(final String elelementName) { Class<?> clazz = null; if (getAliasMap().containsAlias(elelementName) == true) { clazz = aliasMap.getClassForAlias(elelementName); } else { clazz = xmlRegistry.getClassForAlias(elelementName); } if (clazz == null) { try { clazz = Class.forName(elelementName); final Class<?> mappingClass = getImplemenationMapping(clazz); if (mappingClass != null) { clazz = mappingClass; } } catch (final ClassNotFoundException ex) { log.error("Class '" + elelementName + "' not found: " + ex.getMessage()); } } return clazz; } /** * Overload this method for manipulating the object creation for new objects. If null is returned, the object is instantiated * automatically by class type or alias. If this method returns {@link Status#IGNORE} then this object will be ignored from * deserialization. * @param clazz * @param el * @param attrValue * @return Always null. */ protected Object newInstance(final Class<?> clazz, final Element el, final String attrName, final String attrValue) { return null; } protected boolean addCollectionEntry(final Collection<?> col, final Object obj, final Element el) { return false; } protected Object fromString(final IConverter<?> converter, final Element element, final String attrName, final String attrValue) { final Object obj = converter.fromString(attrValue != null ? attrValue : element.getText()); if (attrName != null) { putProcessedAttribute(element, attrName); } else { putProcessedElement(element); } return obj; } @SuppressWarnings({ "unchecked", "rawtypes" }) protected Object enumFromString(final Class<?> clazz, final Element element, final String attrName, final String attrValue) { final String val = attrValue != null ? attrValue : element.getText(); Enum enumValue; if (StringUtils.isBlank(val) || "null".equals(val) == true) { enumValue = null; } else { try { enumValue = Enum.valueOf((Class) clazz, val); } catch (final IllegalArgumentException ex) { // Try toUpperCase: enumValue = Enum.valueOf((Class) clazz, val.toUpperCase()); } } if (attrName != null) { putProcessedAttribute(element, attrName); } else { putProcessedElement(element); } return enumValue; } private Object read(final Class<?> clazz, final Element el, final String attrName, final String attrValue) { final Attribute refIdAttr = el.attribute(XmlObjectWriter.ATTR_REF_ID); if (refIdAttr != null) { final String refId = refIdAttr.getText(); if (StringUtils.isEmpty(refId) == true) { log.error("Invalid ref-id for element '" + el.getName() + "': " + refId); return null; } final Object obj = referenceObjects.get(refId); if (obj == null) { log.error("Oups, can't find referenced object for element '" + el.getName() + "': " + refId); } putProcessedElement(el); putProcessedAttribute(el, refIdAttr.getName()); return obj; } Object value = newInstance(clazz, el, attrName, attrValue); if (value == Status.IGNORE) { return null; } final IConverter<?> converter = xmlRegistry.getConverter(clazz); if (converter != null) { if (value == null) { value = fromString(converter, el, attrName, attrValue); } } else if (Enum.class.isAssignableFrom(clazz) == true) { if (value == null) { value = enumFromString(clazz, el, attrName, attrValue); } } else if (Collection.class.isAssignableFrom(clazz) == true) { Collection<Object> col = null; if (value != null) { @SuppressWarnings("unchecked") final Collection<Object> c = (Collection<Object>) value; col = c; } else if (SortedSet.class.isAssignableFrom(clazz) == true) { col = new TreeSet<Object>(); } else if (Set.class.isAssignableFrom(clazz) == true) { col = new HashSet<Object>(); } else { col = new ArrayList<Object>(); } putProcessedElement(el); for (final Object listObject : el.elements()) { final Element childElement = (Element) listObject; final Object child = read(childElement); if (child != null) { if (addCollectionEntry(col, child, childElement) == false) { col.add(child); } } } if (ignoreEmptyCollections == false || CollectionUtils.isNotEmpty(col) == true) { value = col; } } else { if (value == null) { final Class<?> mappingClass = getImplemenationMapping(clazz); if (mappingClass != null) { value = BeanHelper.newInstance(mappingClass); } else { value = BeanHelper.newInstance(clazz); } } if (value != null) { final Attribute idAttr = el.attribute(XmlObjectWriter.ATTR_ID); if (idAttr != null) { final String id = idAttr.getText(); if (StringUtils.isEmpty(id) == true) { log.error("Invalid id for element '" + el.getName() + "': " + id); } else { this.referenceObjects.put(id, value); } putProcessedAttribute(el, idAttr.getName()); } read(value, el); } } return value; } /** * Please note: Does not set the default values, this should be done by the class itself (when declaring the fields or in the * constructors). * @param obj The object to assign the parameters parsed from the given xml element. * @param str */ public void read(final Object obj, final Element el) { if (el == null) { return; } final Field[] fields = BeanHelper.getAllDeclaredFields(obj.getClass()); AccessibleObject.setAccessible(fields, true); for (final Object listObject : el.attributes()) { final Attribute attr = (Attribute) listObject; final String key = attr.getName(); if (StringHelper.isIn(key, XmlObjectWriter.ATTR_ID, XmlObjectWriter.ATTR_REF_ID) == true) { // Do not try to find fields for o-id and ref-id. continue; } final String value = attr.getText(); proceedElement(obj, fields, el, key, value, true); } for (final Object listObject : el.elements()) { final Element childElement = (Element) listObject; final String key = childElement.getName(); proceedElement(obj, fields, childElement, key, null, false); } putProcessedElement(el); } protected void setField(final Field field, final Object obj, final Object value, final Element element, final String key, final String attrValue) { if (value != null) { setField(field, obj, value); } } protected void setField(final Field field, final Object obj, final Object value) { try { field.set(obj, value); } catch (final IllegalArgumentException ex) { log.fatal("Exception encountered " + ex + ". Ignoring field '" + field.getName() + "' with value '" + value + "' in deserialization of class '" + obj.getClass() + "'.", ex); } catch (final IllegalAccessException ex) { log.fatal("Exception encountered " + ex + ". Ignoring field '" + field.getName() + "' with value '" + value + "' in deserialization of class '" + obj.getClass() + "'.", ex); } } private void proceedElement(final Object obj, final Field[] fields, final Element el, final String key, final String attrValue, final boolean isAttribute) { Field foundField = null; for (final Field field : fields) { if (XmlObjectWriter.ignoreField(field) == true) { continue; } final XmlField ann = field.isAnnotationPresent(XmlField.class) == true ? field.getAnnotation(XmlField.class) : null; if (key.equals(field.getName()) == false && (ann == null || key.equals(ann.alias()) == false)) { continue; } foundField = field; break; } if (foundField != null) { // Field found: final Class<?> type = foundField.getType(); final Object value = read(type, el, key, attrValue); setField(foundField, obj, value, el, key, attrValue); if (isAttribute == true) { putProcessedAttribute(el, key); } else { putProcessedElement(el); } } else { log.warn("Field '" + key + "' not found."); } } private void putProcessedElement(final Element el) { if (processedElements != null) { processedElements.add(el); } } private void putProcessedAttribute(final Element el, final String attrName) { if (processedAttributes != null) { Set<String> attributeSet = processedAttributes.get(el); if (attributeSet == null) { attributeSet = new HashSet<String>(); processedAttributes.put(el, attributeSet); } attributeSet.add(attrName); } } }