/* * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program 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; either version 3 of the License, or (at your option) * any later version. * This program 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 com.bc.ceres.binding.dom; import com.bc.ceres.binding.ConversionException; import com.bc.ceres.binding.Converter; import com.bc.ceres.binding.ConverterRegistry; import com.bc.ceres.binding.DefaultPropertyDescriptorFactory; import com.bc.ceres.binding.DefaultPropertySetDescriptor; import com.bc.ceres.binding.Property; import com.bc.ceres.binding.PropertyContainer; import com.bc.ceres.binding.PropertyDescriptor; import com.bc.ceres.binding.PropertyDescriptorFactory; import com.bc.ceres.binding.PropertySet; import com.bc.ceres.binding.PropertySetDescriptor; import com.bc.ceres.binding.ValidationException; import com.bc.ceres.core.Assert; import java.lang.reflect.Array; import java.lang.reflect.Modifier; import java.util.LinkedHashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; /** * {@inheritDoc} */ public class DefaultDomConverter implements DomConverter { private static final String CLASS_ATTRIBUTE_NAME = "class"; private final Class<?> valueType; private PropertySetDescriptor propertySetDescriptor; private PropertyDescriptorFactory propertyDescriptorFactory; public DefaultDomConverter(Class<?> valueType) { this(valueType, new DefaultPropertyDescriptorFactory()); } public DefaultDomConverter(Class<?> valueType, PropertyDescriptorFactory propertyDescriptorFactory) { this(valueType, propertyDescriptorFactory, null); } public DefaultDomConverter(Class<?> valueType, PropertyDescriptorFactory propertyDescriptorFactory, PropertySetDescriptor propertySetDescriptor) { Assert.notNull(valueType, "valueType"); Assert.notNull(propertyDescriptorFactory, "propertyDescriptorFactory"); this.valueType = valueType; this.propertyDescriptorFactory = propertyDescriptorFactory; this.propertySetDescriptor = propertySetDescriptor; } /** * {@inheritDoc} */ @Override public Class<?> getValueType() { return valueType; } public PropertyDescriptorFactory getPropertyDescriptorFactory() { return propertyDescriptorFactory; } public PropertySetDescriptor getPropertySetDescriptor() { if (propertySetDescriptor == null) { propertySetDescriptor = DefaultPropertySetDescriptor.createFromClass(valueType, propertyDescriptorFactory); } return propertySetDescriptor; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // VALUE --> DOM //////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * {@inheritDoc} */ @Override public void convertValueToDom(Object value, DomElement parentElement) throws ConversionException { PropertySet propertySet = getPropertySet(value); for (Property property : propertySet.getProperties()) { convertPropertyToDom(property, parentElement); } } private void convertPropertyToDom(Property property, DomElement parentElement) throws ConversionException { PropertyDescriptor descriptor = property.getDescriptor(); if (descriptor.isTransient() || descriptor.isDeprecated()) { return; } Object value = property.getValue(); if (value == null) { return; } DomElement childElement = parentElement.createChild(getNameOrAlias(property)); Class<?> type = property.getDescriptor().getType(); Class<?> actualType = value.getClass(); if (isExplicitClassNameRequired(type, value)) { // childValue is an implementation of type and it's not of same type // we have to store the implementation class in order to re-construct the object // but only if type is not an enum. childElement.setAttribute(CLASS_ATTRIBUTE_NAME, actualType.getName()); } ChildConverter childConverter = findChildConverter(descriptor, actualType); if (childConverter == null) { throw new ConversionException(String.format("Don't know how to convert property '%s'", childElement.getName())); } childConverter.convertValueToDom(value, childElement); } private static String getNameOrAlias(Property property) { String alias = property.getDescriptor().getAlias(); if (alias != null && !alias.isEmpty()) { return alias; } return property.getDescriptor().getName(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // DOM --> VALUE //////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * {@inheritDoc} */ @Override public Object convertDomToValue(DomElement parentElement, Object value) throws ConversionException, ValidationException { PropertySet propertySet; if (value == null) { value = createValueInstance(parentElement, getValueType()); propertySet = getPropertySet(value); propertySet.setDefaultValues(); } else { propertySet = getPropertySet(value); } convertDomToPropertySet(parentElement, propertySet); return value; } private void convertDomToPropertySet(DomElement parentElement, PropertySet propertySet) throws ConversionException, ValidationException { for (DomElement childElement : parentElement.getChildren()) { convertDomChildToPropertySet(childElement, propertySet); } } private void convertDomChildToPropertySet(DomElement child, PropertySet propertySet) throws ConversionException, ValidationException { String childName = child.getName(); Property property = propertySet.getProperty(childName); if (property == null) { throw new ConversionException(String.format("Unknown element '%s'", childName)); } if (property.getDescriptor().isTransient()) { return; } convertDomChildToProperty(child, property); } private void convertDomChildToProperty(DomElement childElement, Property property) throws ConversionException, ValidationException { PropertyDescriptor descriptor = property.getDescriptor(); Object currentValue = property.getValue(); Class<?> actualType = getActualType(childElement, currentValue != null ? currentValue.getClass() : null, false); ChildConverter childConverter = findChildConverter(descriptor, actualType); if (childConverter == null) { throw new ConversionException(String.format("Don't know how to convert element '%s'", childElement.getName())); } Object value = childConverter.convertDomToValue(childElement, currentValue); property.setValue(value); } private Object createValueInstance(DomElement parentElement, Class<?> defaultType) throws ConversionException { Class<?> itemType = getActualType(parentElement, defaultType, true); return createValueInstance(itemType); } private Class<?> getActualType(DomElement parentElement, Class<?> defaultType, boolean failIfNotFound) throws ConversionException { Class<?> actualType; String className = parentElement.getAttribute(CLASS_ATTRIBUTE_NAME); if (className != null) { // implementation of an interface ? try { actualType = Thread.currentThread().getContextClassLoader().loadClass(className); } catch (ClassNotFoundException e) { if (failIfNotFound) { throw new ConversionException(e); } // This is ok, type info may not be used at all. actualType = defaultType; } } else { actualType = defaultType; } return actualType; } protected Object createValueInstance(Class<?> type) { if (type == Map.class) { // retain add-order of elements return new LinkedHashMap(); } else if (type == SortedMap.class) { return new TreeMap(); } else { Object childValue; try { childValue = type.newInstance(); } catch (Throwable t) { throw new RuntimeException(String.format("Failed to create instance of %s (default constructor missing?).", type.getName()), t); } return childValue; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // common //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// protected PropertySet getPropertySet(Object value) { PropertySet propertySet; if (value instanceof PropertySet) { propertySet = (PropertySet) value; } else if (value instanceof Map) { propertySet = PropertyContainer.createMapBacked((Map) value, getPropertySetDescriptor()); } else if (value.getClass().equals(getValueType())) { propertySet = PropertyContainer.createObjectBacked(value, getPropertySetDescriptor()); } else { propertySet = PropertyContainer.createObjectBacked(value, getPropertyDescriptorFactory()); } return propertySet; } /** * Called to create a new DOM converter for a (child) property. * May be overridden by subclasses. The default implementation returns an instance of this class. * * @param valueType The value type * @param propertyDescriptorFactory The property descriptor factory. * @param propertySetDescriptor The property set descriptor. * @return a "local" DOM converter or {@code null}. */ protected DomConverter createChildDomConverter(Class<?> valueType, PropertyDescriptorFactory propertyDescriptorFactory, PropertySetDescriptor propertySetDescriptor) { return new DefaultDomConverter(valueType, propertyDescriptorFactory, propertySetDescriptor); } /** * Called to find a "local" DOM converter for a (child) property. * May be overridden by subclasses. The default implementation returns {@code null}. * * @param descriptor The property descriptor * @return a "local" DOM converter or {@code null}. */ protected DomConverter findChildDomConverter(PropertyDescriptor descriptor) { return null; } private ChildConverter findChildConverter(PropertyDescriptor descriptor, Class<?> actualType) { DomConverter domConverter = findChildDomConverter(descriptor); if (domConverter != null) { return new ComplexChildConverter(domConverter); } domConverter = descriptor.getDomConverter(); if (domConverter != null) { return new ComplexChildConverter(domConverter); } PropertySetDescriptor psd = descriptor.getPropertySetDescriptor(); if (psd != null) { return new ComplexChildConverter(createChildDomConverter(descriptor.getType(), getPropertyDescriptorFactory(), psd)); } if (descriptor.getType().isArray()) { boolean hasItemAlias = descriptor.getItemAlias() != null && !descriptor.getItemAlias().isEmpty(); if (hasItemAlias) { String itemName = descriptor.getItemAlias(); Class<?> itemType = descriptor.getType().getComponentType(); PropertyDescriptor itemDescriptor = new PropertyDescriptor(itemName, itemType); ChildConverter itemChildConverter = findChildConverter(itemDescriptor, actualType != null ? actualType.getComponentType() : null); if (itemChildConverter != null) { return new ArrayToDomConverter(itemName, itemType, itemChildConverter); } } } Converter converter = descriptor.getConverter(); if (converter != null) { return new SingleValueChildConverter(converter); } ChildConverter globalChildConverter = findGlobalChildConverter(descriptor.getType()); if (globalChildConverter != null) { return globalChildConverter; } // up to this point we tried to exploit property descriptor attributes // but didn't find a converter. Now we ask the actual type, if any. if (actualType != null) { if (!actualType.equals(descriptor.getType())) { ChildConverter childConverter = findGlobalChildConverter(actualType); if (childConverter != null) { return childConverter; } } return createChildConverter(actualType); } else if (isInstantiable(descriptor.getType())) { return createChildConverter(descriptor.getType()); } return null; } private boolean isInstantiable(Class<?> type) { // Note: we don't check for no-arg constructor here, because we want Java to throw a runtime exception return !type.isInterface() && !Modifier.isAbstract(type.getModifiers()) && !type.isEnum() && !type.isArray() && Modifier.isPublic(type.getModifiers()); } private boolean isExplicitClassNameRequired(Class<?> type, Object value) { return type.isInstance(value) && type != value.getClass() && isInstantiable(value.getClass()); } private ChildConverter createChildConverter(Class<?> actualType) { PropertySetDescriptor actualTypePsd = DefaultPropertySetDescriptor.createFromClass(actualType, getPropertyDescriptorFactory()); return new ComplexChildConverter(createChildDomConverter(actualType, getPropertyDescriptorFactory(), actualTypePsd)); } private static ChildConverter findGlobalChildConverter(Class<?> type) { Converter converter = ConverterRegistry.getInstance().getConverter(type); if (converter != null) { return new SingleValueChildConverter(converter); } DomConverter domConverter = DomConverterRegistry.getInstance().getConverter(type); if (domConverter != null) { return new ComplexChildConverter(domConverter); } return null; } private static interface ChildConverter { void convertValueToDom(Object value, DomElement childElement) throws ConversionException; Object convertDomToValue(DomElement childElement, Object value) throws ConversionException, ValidationException; } private static class SingleValueChildConverter implements ChildConverter { final Converter converter; private SingleValueChildConverter(Converter converter) { this.converter = converter; } @Override public void convertValueToDom(Object value, DomElement childElement) throws ConversionException { String text = converter.format(value); if (text != null && !text.isEmpty()) { childElement.setValue(text); } } @Override public Object convertDomToValue(DomElement childElement, Object value) throws ConversionException { String text = childElement.getValue(); if (text != null) { return converter.parse(text); } else { return null; } } } private static class ComplexChildConverter implements ChildConverter { final DomConverter domConverter; private ComplexChildConverter(DomConverter domConverter) { this.domConverter = domConverter; } @Override public void convertValueToDom(Object value, DomElement childElement) throws ConversionException { domConverter.convertValueToDom(value, childElement); } @Override public Object convertDomToValue(DomElement childElement, Object value) throws ConversionException, ValidationException { return domConverter.convertDomToValue(childElement, value); } } private static class ArrayToDomConverter implements ChildConverter { private final String itemName; private final Class<?> itemType; final ChildConverter itemConverter; private ArrayToDomConverter(String itemName, Class<?> itemType, ChildConverter itemConverter) { this.itemName = itemName; this.itemType = itemType; this.itemConverter = itemConverter; } @Override public void convertValueToDom(Object value, DomElement childElement) throws ConversionException { int arrayLength = Array.getLength(value); for (int i = 0; i < arrayLength; i++) { Object item = Array.get(value, i); DomElement itemDomElement = childElement.createChild(itemName); itemConverter.convertValueToDom(item, itemDomElement); } } @Override public Object convertDomToValue(DomElement childElement, Object value) throws ConversionException, ValidationException { // if and only if an itemAlias is set, we parse the array element-wise DomElement[] itemElements = childElement.getChildren(itemName); if (value == null || itemElements.length != Array.getLength(value)) { value = Array.newInstance(itemType, itemElements.length); } else if (value.getClass().getComponentType() == null) { throw new ConversionException(String.format("Incompatible value type: array of type '%s' expected", itemType.getName())); } else if (!itemType.isAssignableFrom(value.getClass().getComponentType())) { throw new ConversionException(String.format("Incompatible array item type: expected '%s', got '%s'", itemType.getName(), value.getClass().getComponentType())); } for (int i = 0; i < itemElements.length; i++) { Object item = itemConverter.convertDomToValue(itemElements[i], null); Array.set(value, i, item); } return value; } } }