/* * 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.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Currency; import java.util.GregorianCalendar; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import javolution.lang.Reflection; import javolution.text.CharArray; import javolution.xml.XMLBinding; import javolution.xml.XMLFormat; import javolution.xml.XMLSerializable; import javolution.xml.stream.XMLStreamException; import javolution.xml.stream.XMLStreamReader; import javolution.xml.stream.XMLStreamWriter; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import sun.reflect.ReflectionFactory; /** * An {@link XMLBinding} that provides class bindings based on reflection. * * @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a> */ public class ReflectionBinding extends XMLBinding { public static final String CLASS = "class"; private static final long serialVersionUID = -7047053153745571559L; private static final Log LOG = LogFactory.getLog( ReflectionBinding.class ); private static final ReflectionFactory REFLECTION_FACTORY = ReflectionFactory.getReflectionFactory(); private static final Object[] INITARGS = new Object[0]; private static final String SIZE = "size"; private static final XMLCalendarFormat CALENDAR_FORMAT = new XMLCalendarFormat(); private static final XMLCurrencyFormat CURRENCY_FORMAT = new XMLCurrencyFormat(); private final Map<Class<?>, XMLFormat<?>> _formats = new ConcurrentHashMap<Class<?>, XMLFormat<?>>(); private transient final ClassLoader _classLoader; private transient final XMLEnumFormat _enumFormat; private transient final XMLArrayFormat _arrayFormat; private transient final XMLCollectionFormat _collectionFormat; private transient final XMLMapFormat _mapFormat; private transient final XMLJdkProxyFormat _jdkProxyFormat; private transient final CustomXMLFormat<?>[] _customFormats; public ReflectionBinding( final ClassLoader classLoader ) { this( classLoader, false ); } public ReflectionBinding( final ClassLoader classLoader, final boolean copyCollectionsForSerialization, final CustomXMLFormat<?> ... customFormats ) { _classLoader = classLoader; _enumFormat = new XMLEnumFormat( classLoader ); _arrayFormat = new XMLArrayFormat( classLoader ); _collectionFormat = new XMLCollectionFormat( copyCollectionsForSerialization ); _mapFormat = new XMLMapFormat( copyCollectionsForSerialization ); _jdkProxyFormat = new XMLJdkProxyFormat( classLoader ); _customFormats = customFormats; Reflection.getInstance().add( classLoader ); } /** * {@inheritDoc} */ @SuppressWarnings( "unchecked" ) @Override protected void writeClass( Class cls, final XMLStreamWriter writer, final boolean useAttributes ) throws XMLStreamException { if ( Proxy.isProxyClass( cls ) ) { cls = Proxy.class; } CustomXMLFormat<?> xmlFormat = null; if ( ( xmlFormat = getCustomFormat( cls ) ) != null ) { cls = xmlFormat.getTargetClass( cls ); } if ( useAttributes ) { writer.writeAttribute( CLASS, cls.getName() ); } else { writer.writeStartElement( cls.getName() ); } } /** * {@inheritDoc} */ @SuppressWarnings( "unchecked" ) @Override protected Class readClass( final XMLStreamReader reader, final boolean useAttributes ) throws XMLStreamException { final CharArray className = useAttributes ? reader.getAttributeValue( null, CLASS ) : reader.getLocalName(); try { return Class.forName( className.toString(), true, _classLoader ); } catch ( final ClassNotFoundException e ) { throw new XMLStreamException( e ); } } @SuppressWarnings( "unchecked" ) @Override public XMLFormat<?> getFormat( final Class cls ) throws XMLStreamException { XMLFormat<?> xmlFormat = _formats.get( cls ); if ( xmlFormat != null ) { return xmlFormat; } /* after reflection based formats check custom formats. this is * required for the cglib extension, and is useful to allow the user * to overwrite existing formats */ if ( ( xmlFormat = getCustomFormat( cls ) ) != null ) { return xmlFormat; } else if ( cls.isPrimitive() || cls == String.class || cls == Boolean.class || cls == Integer.class || cls == Long.class || cls == Short.class || cls == Double.class || cls == Float.class || cls == Character.class || cls == Byte.class || cls == Class.class ) { return super.getFormat( cls ); } else if ( XMLSerializable.class.isAssignableFrom( cls ) ) { return super.getFormat( cls ); } else if ( cls.isArray() ) { return getArrayFormat( cls ); } else if ( Collection.class.isAssignableFrom( cls ) && Modifier.isPublic( cls.getModifiers() ) ) { // the check for the private modifier is required, so that // lists like Arrays.ArrayList are handled by the ReflectionFormat return _collectionFormat; } else if ( Map.class.isAssignableFrom( cls ) && Modifier.isPublic( cls.getModifiers() ) ) { return _mapFormat; } else if ( cls.isEnum() ) { return _enumFormat; } else if ( Calendar.class.isAssignableFrom( cls ) ) { return CALENDAR_FORMAT; } else if ( Currency.class.isAssignableFrom( cls ) ) { return CURRENCY_FORMAT; } else if ( Proxy.isProxyClass( cls ) || cls == Proxy.class ) { /* the Proxy.isProxyClass check is required for serialization, * Proxy.class is required for deserialization */ return _jdkProxyFormat; } else if ( cls == StringBuilder.class ) { return STRING_BUILDER_FORMAT; } else if ( cls == StringBuffer.class ) { return STRING_BUFFER_FORMAT; } else { if ( xmlFormat == null ) { if ( ReflectionFormat.isNumberFormat( cls ) ) { xmlFormat = ReflectionFormat.getNumberFormat( cls ); } else { xmlFormat = new ReflectionFormat( cls, _classLoader ); } _formats.put( cls, xmlFormat ); } return xmlFormat; } } private CustomXMLFormat<?> getCustomFormat( final Class<?> cls ) { if ( _customFormats == null ) { return null; } for( final CustomXMLFormat<?> xmlFormat : _customFormats ) { if ( xmlFormat.canConvert( cls ) ) { return xmlFormat; } } return null; } @SuppressWarnings( "unchecked" ) private XMLFormat getArrayFormat( final Class cls ) { if ( cls == int[].class ) { return XMLArrayFormats.INT_ARRAY_FORMAT; } else if ( cls == long[].class ) { return XMLArrayFormats.LONG_ARRAY_FORMAT; } else if ( cls == short[].class ) { return XMLArrayFormats.SHORT_ARRAY_FORMAT; } else if ( cls == float[].class ) { return XMLArrayFormats.FLOAT_ARRAY_FORMAT; } else if ( cls == double[].class ) { return XMLArrayFormats.DOUBLE_ARRAY_FORMAT; } else if ( cls == char[].class ) { return XMLArrayFormats.CHAR_ARRAY_FORMAT; } else if ( cls == byte[].class ) { return XMLArrayFormats.BYTE_ARRAY_FORMAT; } else { return _arrayFormat; } } static class XMLEnumFormat extends XMLFormat<Enum<?>> { private final ClassLoader _classLoader; public XMLEnumFormat( final ClassLoader classLoader ) { super( null ); _classLoader = classLoader; } /** * {@inheritDoc} */ @Override public Enum<?> newInstance( final Class<Enum<?>> clazz, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { final String value = xml.getAttribute( "value" ).toString(); final String clazzName = xml.getAttribute( "type" ).toString(); try { @SuppressWarnings( "unchecked" ) final Enum<?> enumValue = Enum.valueOf( Class.forName( clazzName, true, _classLoader ).asSubclass( Enum.class ), value ); return enumValue; } catch ( final ClassNotFoundException e ) { throw new XMLStreamException( e ); } } /** * {@inheritDoc} */ @Override public void read( final javolution.xml.XMLFormat.InputElement xml, final Enum<?> object ) throws XMLStreamException { } /** * {@inheritDoc} */ @Override public void write( final Enum<?> object, final javolution.xml.XMLFormat.OutputElement xml ) throws XMLStreamException { xml.setAttribute( "value", object.name() ); xml.setAttribute( "type", object.getClass().getName() ); } } public static class XMLArrayFormat extends XMLFormat<Object[]> { private final ClassLoader _classLoader; public XMLArrayFormat( final ClassLoader classLoader ) { super( null ); _classLoader = classLoader; } /** * {@inheritDoc} */ @SuppressWarnings( "unchecked" ) @Override public Object[] newInstance( final Class clazz, final javolution.xml.XMLFormat.InputElement input ) throws XMLStreamException { try { final String componentType = input.getAttribute( "componentType", (String) null ); final int length = input.getAttribute( "length", 0 ); return (Object[]) Array.newInstance( Class.forName( componentType, false, _classLoader ), length ); } catch ( final Exception e ) { LOG.error( "caught exception", e ); throw new XMLStreamException( e ); } } @Override public void read( final javolution.xml.XMLFormat.InputElement input, final Object[] array ) throws XMLStreamException { int i = 0; while ( input.hasNext() ) { array[i++] = input.getNext(); } } @Override public final void write( final Object[] array, final javolution.xml.XMLFormat.OutputElement output ) throws XMLStreamException { output.setAttribute( "type", "array" ); output.setAttribute( "componentType", array.getClass().getComponentType().getName() ); output.setAttribute( "length", array.length ); writeElements( array, output ); } public void writeElements( final Object[] array, final javolution.xml.XMLFormat.OutputElement output ) throws XMLStreamException { for ( final Object item : array ) { output.add( item ); } } } public static class XMLCollectionFormat extends XMLFormat<Collection<Object>> { private final boolean _copyForWrite; protected XMLCollectionFormat( final boolean copyForWrite ) { super( null ); _copyForWrite = copyForWrite; } /** * {@inheritDoc} */ @SuppressWarnings( "unchecked" ) @Override public Collection<Object> newInstance( final Class<Collection<Object>> cls, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { Collection<Object> result = newInstanceFromPublicConstructor( cls, xml ); if ( result == null && Modifier.isPrivate( cls.getModifiers() ) ) { try { final Constructor<?> constructor = REFLECTION_FACTORY.newConstructorForSerialization( cls, Object.class.getDeclaredConstructor( new Class[0] ) ); constructor.setAccessible( true ); return (Collection<Object>) constructor.newInstance( INITARGS ); } catch ( final Exception e ) { throw new XMLStreamException( e ); } } if ( result == null ) { result = super.newInstance( cls, xml ); } return result; } @Override public void read( final javolution.xml.XMLFormat.InputElement xml, final Collection<Object> obj ) throws XMLStreamException { while ( xml.hasNext() ) { obj.add( xml.getNext() ); } } @Override public void write( final Collection<Object> obj, final javolution.xml.XMLFormat.OutputElement xml ) throws XMLStreamException { xml.setAttribute( SIZE, obj.size() ); for ( final Object item : _copyForWrite ? new ArrayList<Object>( obj ) : obj ) { xml.add( item ); } } } public static class XMLMapFormat extends XMLFormat<Map<Object,Object>> { private final boolean _copyForWrite; protected XMLMapFormat( final boolean copyForWrite ) { super( null ); _copyForWrite = copyForWrite; } /** * {@inheritDoc} */ @SuppressWarnings( "unchecked" ) @Override public Map<Object, Object> newInstance( final Class<Map<Object, Object>> cls, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { Map<Object, Object> result = newInstanceFromPublicConstructor( cls, xml ); if ( result == null && Modifier.isPrivate( cls.getModifiers() ) ) { try { final Constructor<?> constructor = REFLECTION_FACTORY.newConstructorForSerialization( cls, Object.class.getDeclaredConstructor( new Class[0] ) ); constructor.setAccessible( true ); result = (Map<Object, Object>) constructor.newInstance( INITARGS ); } catch ( final Exception e ) { throw new XMLStreamException( e ); } } if ( result == null ) { result = super.newInstance( cls, xml ); } return result; } @Override public void read( final javolution.xml.XMLFormat.InputElement xml, final Map<Object,Object> obj ) throws XMLStreamException { while ( xml.hasNext() ) { obj.put(xml.get("k"), xml.get("v")); } } @Override public void write( final Map<Object,Object> obj, final javolution.xml.XMLFormat.OutputElement xml ) throws XMLStreamException { xml.setAttribute( SIZE, obj.size() ); final Set<Entry<Object, Object>> entrySet = _copyForWrite ? new LinkedHashMap<Object, Object>( obj ).entrySet() : obj.entrySet(); for ( final Map.Entry<Object, Object> entry : entrySet ) { xml.add( entry.getKey(), "k" ); xml.add( entry.getValue(), "v" ); } } } @SuppressWarnings( "unchecked" ) private static <T> T newInstanceFromPublicConstructor( final Class<T> cls, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { try { final Constructor<?>[] constructors = cls.getConstructors(); for ( final Constructor<?> constructor : constructors ) { final Class<?>[] parameterTypes = constructor.getParameterTypes(); if ( parameterTypes.length == 0 ) { return (T) constructor.newInstance(); } else if ( parameterTypes.length == 1 && parameterTypes[0] == int.class ) { final CharArray size = xml.getAttribute( SIZE ); if ( size != null ) { return (T) constructor.newInstance( size.toInt() ); } } } if ( LOG.isDebugEnabled() && constructors.length > 0 ) { LOG.debug( "No suitable constructor found for map " + cls + ", available constructors:\n" + Arrays.asList( constructors ) ); } } catch ( final SecurityException e ) { // ignore } catch ( final IllegalArgumentException e ) { throw new XMLStreamException( e ); // not expected } catch ( final InstantiationException e ) { throw new XMLStreamException( e ); // not expected } catch ( final IllegalAccessException e ) { throw new XMLStreamException( e ); // not expected } catch ( final InvocationTargetException e ) { // ignore - constructor threw exception LOG.info( "Tried to invoke int constructor on " + cls.getName() + ", this threw an exception.", e.getTargetException() ); } return null; } public static class XMLCurrencyFormat extends XMLFormat<Currency> { public XMLCurrencyFormat() { super( Currency.class ); } /** * Currency instance do not have to be handled by the reference resolver, as we're using * Currency.getInstance for retrieving an instance. * * @return <code>false</code> */ @Override public boolean isReferenceable() { return false; } @Override public Currency newInstance( final Class<Currency> cls, final javolution.xml.XMLFormat.InputElement xml ) throws XMLStreamException { return Currency.getInstance( xml.getAttribute( "code", "" ) ); } public void write( final Currency currency, final OutputElement xml ) throws XMLStreamException { xml.setAttribute( "code", currency.getCurrencyCode() ); } public void read( final InputElement xml, final Currency pos ) { // Immutable, deserialization occurs at creation, ref. newIntance(...) } } /** * An {@link XMLFormat} for {@link Calendar} that serialized those calendar * fields that contain actual data (these fields also are used by * {@link Calendar#equals(Object)}. */ public static class XMLCalendarFormat extends XMLFormat<Calendar> { private final Field _zoneField; public XMLCalendarFormat() { super( Calendar.class ); try { _zoneField = Calendar.class.getDeclaredField( "zone" ); _zoneField.setAccessible( true ); } catch ( final Exception e ) { throw new RuntimeException( e ); } } @Override public Calendar newInstance( final Class<Calendar> clazz, final javolution.xml.XMLFormat.InputElement arg1 ) throws XMLStreamException { if ( clazz.equals( GregorianCalendar.class ) ) { return GregorianCalendar.getInstance(); } throw new IllegalArgumentException( "Calendar of type " + clazz.getName() + " not yet supported. Please submit an issue so that it will be implemented." ); } @Override public void read( final javolution.xml.XMLFormat.InputElement xml, final Calendar obj ) throws XMLStreamException { /* check if we actually need to set the timezone, as * TimeZone.getTimeZone is synchronized, so we might prevent this */ final String timeZoneId = xml.getAttribute( "tz", "" ); if ( !getTimeZone( obj ).getID().equals( timeZoneId ) ) { obj.setTimeZone( TimeZone.getTimeZone( timeZoneId ) ); } obj.setMinimalDaysInFirstWeek( xml.getAttribute( "minimalDaysInFirstWeek", -1 ) ); obj.setFirstDayOfWeek( xml.getAttribute( "firstDayOfWeek", -1 ) ); obj.setLenient( xml.getAttribute( "lenient", true ) ); obj.setTimeInMillis( xml.getAttribute( "timeInMillis", -1L ) ); } @Override public void write( final Calendar obj, final javolution.xml.XMLFormat.OutputElement xml ) throws XMLStreamException { if ( !obj.getClass().equals( GregorianCalendar.class ) ) { throw new IllegalArgumentException( "Calendar of type " + obj.getClass().getName() + " not yet supported. Please submit an issue so that it will be implemented." ); } xml.setAttribute( "timeInMillis", obj.getTimeInMillis() ); xml.setAttribute( "lenient", obj.isLenient() ); xml.setAttribute( "firstDayOfWeek", obj.getFirstDayOfWeek() ); xml.setAttribute( "minimalDaysInFirstWeek", obj.getMinimalDaysInFirstWeek() ); xml.setAttribute( "tz", getTimeZone( obj ).getID() ); } private TimeZone getTimeZone( final Calendar obj ) throws XMLStreamException { /* access the timezone via the field, to prevent cloning of the tz */ try { return (TimeZone) _zoneField.get( obj ); } catch ( final Exception e ) { throw new XMLStreamException( e ); } } } public static final class XMLJdkProxyFormat extends XMLFormat<Object> { private final ClassLoader _classLoader; public XMLJdkProxyFormat( final ClassLoader classLoader ) { super( null ); _classLoader = classLoader; } /** * {@inheritDoc} */ @Override public boolean isReferenceable() { return false; } /** * {@inheritDoc} */ @Override public Object newInstance( final Class<Object> clazz, final javolution.xml.XMLFormat.InputElement input ) throws XMLStreamException { final InvocationHandler invocationHandler = input.get( "handler" ); final Class<?>[] interfaces = getInterfaces( input, "interfaces", _classLoader ); return Proxy.newProxyInstance( _classLoader, interfaces, invocationHandler ); } public static Class<?>[] getInterfaces( final javolution.xml.XMLFormat.InputElement input, final String elementName, final ClassLoader classLoader ) throws XMLStreamException { final String[] interfaceNames = input.get( elementName ); if ( interfaceNames != null ) { try { final Class<?>[] interfaces = new Class<?>[interfaceNames.length]; for ( int i = 0; i < interfaceNames.length; i++ ) { interfaces[i] = Class.forName( interfaceNames[i], true, classLoader ); } return interfaces; } catch ( final ClassNotFoundException e ) { throw new XMLStreamException( e ); } } return new Class<?>[0]; } @Override public void read( final javolution.xml.XMLFormat.InputElement input, final Object obj ) throws XMLStreamException { // nothing to do } @Override public final void write( final Object obj, final javolution.xml.XMLFormat.OutputElement output ) throws XMLStreamException { final InvocationHandler invocationHandler = Proxy.getInvocationHandler( obj ); output.add( invocationHandler, "handler" ); final String[] interfaceNames = getInterfaceNames( obj ); output.add( interfaceNames, "interfaces" ); } public static String[] getInterfaceNames( final Object obj ) { final Class<?>[] interfaces = obj.getClass().getInterfaces(); if ( interfaces != null ) { final String[] interfaceNames = new String[interfaces.length]; for ( int i = 0; i < interfaces.length; i++ ) { interfaceNames[i] = interfaces[i].getName(); } return interfaceNames; } return new String[0]; } } public static final XMLFormat<StringBuilder> STRING_BUILDER_FORMAT = new XMLFormat<StringBuilder>( StringBuilder.class ) { public StringBuilder newInstance( final Class<StringBuilder> cls, final InputElement xml ) throws XMLStreamException { return new StringBuilder( xml.getAttribute( "val" ) ); } @Override public void read( final InputElement xml, final StringBuilder obj ) throws XMLStreamException { // nothing todo } @Override public void write( final StringBuilder obj, final OutputElement xml ) throws XMLStreamException { xml.setAttribute( "val", obj.toString() ); } }; public static final XMLFormat<StringBuffer> STRING_BUFFER_FORMAT = new XMLFormat<StringBuffer>( StringBuffer.class ) { public StringBuffer newInstance( final Class<StringBuffer> cls, final InputElement xml ) throws XMLStreamException { return new StringBuffer( xml.getAttribute( "val" ) ); } @Override public void read( final InputElement xml, final StringBuffer obj ) throws XMLStreamException { // nothing todo } @Override public void write( final StringBuffer obj, final OutputElement xml ) throws XMLStreamException { xml.setAttribute( "val", obj.toString() ); } }; }