/* * Copyright 2009 Martin Grotzke * * Licensed 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 de.javakaffee.web.msm.serializer.javolution; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Currency; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javolution.text.CharArray; import javolution.text.TypeFormat; import javolution.xml.XMLFormat; import javolution.xml.sax.Attributes; import javolution.xml.stream.XMLStreamException; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import sun.reflect.ReflectionFactory; /** * An {@link XMLFormat} that provides the binding for a certain class to to/from * xml based on reflection. * <p> * When serializing an object to xml, the values of the declared fields are read * (including inherited fields) from the object. Fields marked as * <code>transient</code> or <code>static</code> are omitted. * </p> * <p> * During deserialization, first all attributes contained in the xml are read * and written to the object. Afterwards the fields that are bound to elements * are checked for contained xml elements and in this case the values are * written to the object. * </p> * * @param <T> the type that is read/written by this {@link XMLFormat}. * * @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a> */ public class ReflectionFormat<T> extends XMLFormat<T> { private static final Log LOG = LogFactory.getLog( ReflectionFormat.class ); private static final Map<Class<?>, XMLNumberFormat<?>> NUMBER_FORMATS = new ConcurrentHashMap<Class<?>, XMLNumberFormat<?>>(); private static final ReflectionFactory REFLECTION_FACTORY = ReflectionFactory.getReflectionFactory(); private static final Object[] INITARGS = new Object[0]; private final Constructor<T> _constructor; private final AttributeHandler[] _attributes; private final Field[] _elements; private final Map<String, Field> _attributesMap; /** * Creates a new instance for the provided class. * * @param clazz * the Class that is supported by this {@link XMLFormat}. * @param classLoader * the {@link ClassLoader} that is used to load user types. */ @SuppressWarnings( "unchecked" ) public ReflectionFormat( final Class<T> clazz, final ClassLoader classLoader ) { super( null ); try { _constructor = (Constructor<T>)REFLECTION_FACTORY.newConstructorForSerialization( clazz, Object.class.getDeclaredConstructor( new Class[0] ) ); _constructor.setAccessible( true ); } catch ( final SecurityException e ) { throw new RuntimeException( e ); } catch ( final NoSuchMethodException e ) { throw new RuntimeException( e ); } final AttributesAndElements fields = allFields( clazz ); _attributes = fields.attributes.toArray( new AttributeHandler[fields.attributes.size()] ); _elements = fields.elements.toArray( new Field[fields.elements.size()] ); // no concurrency support required here, as we'll only read from the map _attributesMap = new HashMap<String, Field>( _attributes.length + 1 ); for ( final AttributeHandler attribute : _attributes ) { _attributesMap.put( attribute._field.getName(), attribute._field ); } } private AttributesAndElements allFields( final Class<T> cls ) { final AttributesAndElements result = new AttributesAndElements(); Class<? super T> clazz = cls; while ( clazz != null ) { addDeclaredFields( clazz, result ); clazz = clazz.getSuperclass(); } return result; } private void addDeclaredFields( final Class<? super T> clazz, final AttributesAndElements result ) { final Field[] declaredFields = clazz.getDeclaredFields(); for ( final Field field : declaredFields ) { if ( !Modifier.isTransient( field.getModifiers() ) && !Modifier.isStatic( field.getModifiers() ) ) { field.setAccessible( true ); result.add( field ); } } } /** * A helper class to collect fields that are serialized as elements and * fields (or {@link AttributeHandler}s for fields) that are serialized * as attributes. */ static class AttributesAndElements { private final Collection<AttributeHandler> attributes; private final Collection<Field> elements; AttributesAndElements() { attributes = new ArrayList<AttributeHandler>(); elements = new ArrayList<Field>(); } void add( final Field field ) { if ( isAttribute( field ) ) { final Class<?> fieldType = field.getType(); if ( fieldType.isPrimitive() ) { if ( fieldType == boolean.class ) { attributes.add( new BooleanAttributeHandler( field ) ); } else if ( fieldType == int.class ) { attributes.add( new IntAttributeHandler( field ) ); } else if ( fieldType == long.class ) { attributes.add( new LongAttributeHandler( field ) ); } else if ( fieldType == float.class ) { attributes.add( new FloatAttributeHandler( field ) ); } else if ( fieldType == double.class ) { attributes.add( new DoubleAttributeHandler( field ) ); } else if ( fieldType == byte.class ) { attributes.add( new ByteAttributeHandler( field ) ); } else if ( fieldType == char.class ) { attributes.add( new CharAttributeHandler( field ) ); } else if ( fieldType == short.class ) { attributes.add( new ShortAttributeHandler( field ) ); } } else { if ( fieldType == String.class || fieldType == Character.class || fieldType == Boolean.class || Number.class.isAssignableFrom( fieldType ) || fieldType == Currency.class ) { attributes.add( new ToStringAttributeHandler( field ) ); } else if ( fieldType.isEnum() ) { attributes.add( new EnumAttributeHandler( field ) ); } else { throw new IllegalArgumentException( "Not yet supported as attribute: " + fieldType ); } } } else { elements.add( field ); } } } protected static boolean isAttribute( final Field field ) { return isAttribute( field.getType() ); } protected static boolean isAttribute( final Class<?> clazz ) { return clazz.isPrimitive() || clazz.isEnum() || clazz == String.class || clazz == Boolean.class || clazz == Integer.class || clazz == Long.class || clazz == Short.class || clazz == Double.class || clazz == Float.class || clazz == Character.class || clazz == Byte.class || clazz == Currency.class; } /** * {@inheritDoc} */ @Override public T newInstance( final Class<T> clazz, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { try { return _constructor.newInstance( INITARGS ); } catch ( final Exception e ) { throw new XMLStreamException( e ); } } /** * {@inheritDoc} */ @Override public void read( final javolution.xml.XMLFormat.InputElement input, final T obj ) throws XMLStreamException { readAttributes( input, obj ); readElements( input, obj ); } private void readAttributes( final javolution.xml.XMLFormat.InputElement input, final T obj ) throws XMLStreamException { final Attributes attributes = input.getAttributes(); for ( int i = 0; i < attributes.getLength(); i++ ) { final CharArray name = attributes.getLocalName( i ); if ( !name.equals( "class" ) && !name.equals( JavolutionTranscoder.REFERENCE_ATTRIBUTE_ID ) ) { final Field field = _attributesMap.get( name.toString() ); if ( field != null ) { setFieldFromAttribute( obj, field, input ); } else { LOG.warn( "Did not find field " + name + ", attribute value is " + attributes.getValue( i ) ); } } } } private void readElements( final javolution.xml.XMLFormat.InputElement input, final T obj ) { for ( final Field field : _elements ) { try { final Object value = input.get( field.getName() ); field.set( obj, value ); } catch ( final Exception e ) { LOG.error( "Could not set field value for field " + field, e ); } } } /** * {@inheritDoc} */ @Override public void write( final T obj, final javolution.xml.XMLFormat.OutputElement output ) throws XMLStreamException { writeAttributes( obj, output ); writeElements( obj, output ); } private void writeAttributes( final T obj, final javolution.xml.XMLFormat.OutputElement output ) { for ( final AttributeHandler handler : _attributes ) { try { handler.writeAttribute( obj, output ); } catch ( final Exception e ) { LOG.error( "Could not set attribute from field value.", e ); } } } private void writeElements( final T obj, final javolution.xml.XMLFormat.OutputElement output ) { for ( final Field field : _elements ) { try { final Object object = field.get( obj ); if ( object != null ) { output.add( object, field.getName() ); } } catch ( final Exception e ) { LOG.error( "Could not write element for field.", e ); } } } static abstract class AttributeHandler { protected final Field _field; public AttributeHandler( final Field field ) { _field = field; } abstract void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException; } static final class BooleanAttributeHandler extends AttributeHandler { public BooleanAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { output.setAttribute( _field.getName(), _field.getBoolean( obj ) ); } } static final class IntAttributeHandler extends AttributeHandler { public IntAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { output.setAttribute( _field.getName(), _field.getInt( obj ) ); } } static final class LongAttributeHandler extends AttributeHandler { public LongAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { output.setAttribute( _field.getName(), _field.getLong( obj ) ); } } static final class FloatAttributeHandler extends AttributeHandler { public FloatAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { output.setAttribute( _field.getName(), _field.getFloat( obj ) ); } } static final class DoubleAttributeHandler extends AttributeHandler { public DoubleAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { output.setAttribute( _field.getName(), _field.getDouble( obj ) ); } } static final class ByteAttributeHandler extends AttributeHandler { public ByteAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { output.setAttribute( _field.getName(), _field.getByte( obj ) ); } } static final class CharAttributeHandler extends AttributeHandler { public CharAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { output.setAttribute( _field.getName(), _field.getChar( obj ) ); } } static final class ShortAttributeHandler extends AttributeHandler { public ShortAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { output.setAttribute( _field.getName(), _field.getShort( obj ) ); } } static abstract class ObjectAttributeHandler extends AttributeHandler { public ObjectAttributeHandler( final Field field ) { super( field ); } @Override void writeAttribute( final Object obj, final XMLFormat.OutputElement output ) throws IllegalArgumentException, XMLStreamException, IllegalAccessException { final Object object = _field.get( obj ); if ( object != null ) { add( object, output ); } } abstract void add( Object object, OutputElement output ) throws XMLStreamException; } static final class ToStringAttributeHandler extends ObjectAttributeHandler { public ToStringAttributeHandler( final Field field ) { super( field ); } @Override void add( final Object object, final OutputElement output ) throws XMLStreamException { output.setAttribute( _field.getName(), object.toString() ); } } static final class EnumAttributeHandler extends ObjectAttributeHandler { public EnumAttributeHandler( final Field field ) { super( field ); } @Override void add( final Object object, final OutputElement output ) throws XMLStreamException { output.setAttribute( _field.getName(), ( (Enum<?>) object ).name() ); } } @edu.umd.cs.findbugs.annotations.SuppressWarnings( "REC_CATCH_EXCEPTION" ) private void setFieldFromAttribute( final T obj, final Field field, final javolution.xml.XMLFormat.InputElement input ) { try { final String fieldName = field.getName(); final Class<?> fieldType = field.getType(); if ( fieldType.isPrimitive() ) { if ( fieldType == boolean.class ) { field.setBoolean( obj, input.getAttribute( fieldName, false ) ); } else if ( fieldType == int.class ) { field.setInt( obj, input.getAttribute( fieldName, 0 ) ); } else if ( fieldType == long.class ) { field.setLong( obj, input.getAttribute( fieldName, (long) 0 ) ); } else if ( fieldType == float.class ) { field.setFloat( obj, input.getAttribute( fieldName, (float) 0 ) ); } else if ( fieldType == double.class ) { field.setDouble( obj, input.getAttribute( fieldName, (double) 0 ) ); } else if ( fieldType == byte.class ) { field.setByte( obj, input.getAttribute( fieldName, (byte) 0 ) ); } else if ( fieldType == char.class ) { field.setChar( obj, input.getAttribute( fieldName, (char) 0 ) ); } else if ( fieldType == short.class ) { field.setShort( obj, input.getAttribute( fieldName, (short) 0 ) ); } } else if ( fieldType.isEnum() ) { final String value = input.getAttribute( fieldName, (String) null ); if ( value != null ) { @SuppressWarnings( "unchecked" ) final Enum<?> enumValue = Enum.valueOf( fieldType.asSubclass( Enum.class ), value ); field.set( obj, enumValue ); } } else { final CharArray object = input.getAttribute( fieldName ); if ( object != null ) { if ( fieldType == String.class ) { field.set( obj, getAttribute( input, fieldName, (String) null ) ); } else if ( fieldType.isAssignableFrom( Boolean.class ) ) { field.set( obj, getAttribute( input, fieldName, (Boolean) null ) ); field.set( obj, getAttribute( input, fieldName, (Boolean) null ) ); } else if ( fieldType.isAssignableFrom( Integer.class ) ) { field.set( obj, getAttribute( input, fieldName, (Integer) null ) ); } else if ( fieldType.isAssignableFrom( Long.class ) ) { field.set( obj, getAttribute( input, fieldName, (Long) null ) ); } else if ( fieldType.isAssignableFrom( Short.class ) ) { field.set( obj, getAttribute( input, fieldName, (Short) null ) ); } else if ( fieldType.isAssignableFrom( Double.class ) ) { field.set( obj, getAttribute( input, fieldName, (Double) null ) ); } else if ( fieldType.isAssignableFrom( Float.class ) ) { field.set( obj, getAttribute( input, fieldName, (Float) null ) ); } else if ( fieldType.isAssignableFrom( Byte.class ) ) { field.set( obj, getAttribute( input, fieldName, (Byte) null ) ); } else if ( fieldType.isAssignableFrom( Character.class ) ) { field.set( obj, getAttribute( input, fieldName, (Character) null ) ); } else if ( Number.class.isAssignableFrom( fieldType ) ) { @SuppressWarnings( "unchecked" ) final XMLNumberFormat<?> format = getNumberFormat( (Class<? extends Number>) fieldType ); field.set( obj, format.newInstanceFromAttribute( input, fieldName ) ); } else if ( fieldType == Currency.class ) { field.set( obj, Currency.getInstance( object.toString() ) ); } else { throw new IllegalArgumentException( "Not yet supported as attribute: " + fieldType ); } } } } catch ( final Exception e ) { try { LOG.error( "Caught exception when trying to set field ("+ field +") from attribute ("+ input.getAttribute( field.getName() )+").", e ); } catch ( final XMLStreamException e1 ) { // fail silently } } } private String getAttribute( final InputElement input, final String name, final String defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); return value != null ? value.toString() : defaultValue; } private Boolean getAttribute( final InputElement input, final String name, final Boolean defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); return value != null ? Boolean.valueOf( value.toBoolean() ) : defaultValue; } private Integer getAttribute( final InputElement input, final String name, final Integer defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); return value != null ? Integer.valueOf( value.toInt() ) : defaultValue; } private Long getAttribute( final InputElement input, final String name, final Long defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); return value != null ? Long.valueOf( value.toLong() ) : defaultValue; } private Short getAttribute( final InputElement input, final String name, final Short defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); return value != null ? Short.valueOf( TypeFormat.parseShort( value ) ) : defaultValue; } private Float getAttribute( final InputElement input, final String name, final Float defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); return value != null ? Float.valueOf( value.toFloat() ) : defaultValue; } private Double getAttribute( final InputElement input, final String name, final Double defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); return value != null ? Double.valueOf( value.toDouble() ) : defaultValue; } private Byte getAttribute( final InputElement input, final String name, final Byte defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); return value != null ? Byte.valueOf( TypeFormat.parseByte( value ) ) : defaultValue; } private Character getAttribute( final InputElement input, final String name, final Character defaultValue ) throws XMLStreamException { final CharArray value = input.getAttribute( name ); if ( value != null ) { if ( value.length() > 1 ) { throw new XMLStreamException( "The attribute '" + name + "' of type Character has illegal value (length > 1): " + value ); } return Character.valueOf( value.charAt( 0 ) ); } return defaultValue; } /** * Used to determine, if the given class can be serialized using the * {@link XMLNumberFormat}. * * @param clazz * the class that is to be checked * @return */ static boolean isNumberFormat( final Class<?> clazz ) { return Number.class.isAssignableFrom( clazz ); } static XMLNumberFormat<?> getNumberFormat( final Class<? extends Number> clazz ) { XMLNumberFormat<? extends Number> result = NUMBER_FORMATS.get( clazz ); if ( result == null ) { result = createNumberFormat( clazz ); NUMBER_FORMATS.put( clazz, result ); } return result; } @SuppressWarnings( "unchecked" ) static <T extends Number> XMLNumberFormat<T> createNumberFormat( final Class<T> clazz ) { try { for ( final Constructor<?> constructor : clazz.getConstructors() ) { final Class<?>[] parameterTypes = constructor.getParameterTypes(); if ( parameterTypes.length == 1 ) { if ( parameterTypes[0] == long.class ) { return new XMLNumberLongFormat<T>( (Constructor<T>) constructor ); } if ( parameterTypes[0] == int.class ) { return new XMLNumberIntFormat<T>( (Constructor<T>) constructor ); } } } } catch ( final Exception e ) { throw new RuntimeException( e ); } throw new IllegalArgumentException( "No suitable constructor found for class " + clazz.getName() + ".\n" + "Available constructors: " + Arrays.toString( clazz.getConstructors() ) ); } /** * The base class for number formats. * * @param <T> * the number type. */ static abstract class XMLNumberFormat<T extends Number> extends XMLFormat<T> { private final Constructor<T> _constructor; public XMLNumberFormat( final Constructor<T> constructor ) { super( null ); _constructor = constructor; } /** * Creates a new instance from the associated constructor. The provided * class is ignored, just the provided {@link InputElement} is used to * read the value which will be passed to the constructor. * * @param clazz * can be null for this {@link XMLFormat} implementation * @param xml * the input element for the object to create. * @return a new number instance. */ @Override public T newInstance( final Class<T> clazz, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { return newInstanceFromAttribute( xml, "value" ); } /** * Creates a new instance from an already associated constructor. The * provided {@link InputElement} is used to read the value from the * attribute with the provided name. The value read will be passed to * the constructor of the object to create. * * @param xml * the input element for the object to create. * @param name * the attribute name to read the value from. * @return a new number instance. */ public T newInstanceFromAttribute( final javolution.xml.XMLFormat.InputElement xml, final String name ) throws XMLStreamException { final Object value = getAttribute( name, xml ); try { return _constructor.newInstance( value ); } catch ( final Exception e ) { throw new XMLStreamException( e ); } } protected abstract Object getAttribute( String name, InputElement xml ) throws XMLStreamException; /** * Does not perform anything, as the number is already created in * {@link #newInstance(Class, javolution.xml.XMLFormat.InputElement)}. * * @param xml * the input element * @param the * obj the created number object */ @Override public void read( final javolution.xml.XMLFormat.InputElement xml, final T obj ) throws XMLStreamException { // nothing to do... } /** * {@inheritDoc} */ @Override public void write( final T obj, final javolution.xml.XMLFormat.OutputElement xml ) throws XMLStreamException { xml.setAttribute( "value", obj.longValue() ); } } static class XMLNumberIntFormat<T extends Number> extends XMLNumberFormat<T> { public XMLNumberIntFormat( final Constructor<T> constructor ) { super( constructor ); } /** * {@inheritDoc} */ @Override public Object getAttribute( final String name, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { return xml.getAttribute( name, 0 ); } } static class XMLNumberLongFormat<T extends Number> extends XMLNumberFormat<T> { public XMLNumberLongFormat( final Constructor<T> constructor ) { super( constructor ); } /** * {@inheritDoc} */ @Override public Object getAttribute( final String name, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { return xml.getAttribute( name, 0L ); } } }