/*******************************************************************************
* Copyright (c) 2010-present Sonatype, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
*******************************************************************************/
package org.eclipse.sisu.plexus;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.inject.Inject;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
import org.codehaus.plexus.util.xml.pull.MXParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.eclipse.sisu.bean.BeanProperties;
import org.eclipse.sisu.bean.BeanProperty;
import org.eclipse.sisu.inject.Logs;
import org.eclipse.sisu.inject.TypeArguments;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.spi.TypeConverter;
import com.google.inject.spi.TypeConverterBinding;
/**
* {@link PlexusBeanConverter} {@link Module} that converts Plexus XML configuration into beans.
*/
@Singleton
public final class PlexusXmlBeanConverter
implements PlexusBeanConverter
{
// ----------------------------------------------------------------------
// Constants
// ----------------------------------------------------------------------
private static final String CONVERSION_ERROR = "Cannot convert: \"%s\" to: %s";
// ----------------------------------------------------------------------
// Implementation fields
// ----------------------------------------------------------------------
private final Collection<TypeConverterBinding> typeConverterBindings;
// ----------------------------------------------------------------------
// Constructors
// ----------------------------------------------------------------------
@Inject
PlexusXmlBeanConverter( final Injector injector )
{
typeConverterBindings = injector.getTypeConverterBindings();
}
// ----------------------------------------------------------------------
// Public methods
// ----------------------------------------------------------------------
@SuppressWarnings( { "unchecked", "rawtypes" } )
public Object convert( final TypeLiteral role, final String value )
{
if ( value.trim().startsWith( "<" ) )
{
try
{
final MXParser parser = new MXParser();
parser.setInput( new StringReader( value ) );
parser.nextTag();
return parse( parser, role );
}
catch ( final Exception e )
{
throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, role ), e );
}
}
return convertText( value, role );
}
// ----------------------------------------------------------------------
// Implementation methods
// ----------------------------------------------------------------------
/**
* Parses a sequence of XML elements and converts them to the given target type.
*
* @param parser The XML parser
* @param toType The target type
* @return Converted instance of the target type
*/
private Object parse( final MXParser parser, final TypeLiteral<?> toType )
throws Exception
{
parser.require( XmlPullParser.START_TAG, null, null );
final Class<?> rawType = toType.getRawType();
if ( Xpp3Dom.class.isAssignableFrom( rawType ) )
{
return parseXpp3Dom( parser );
}
if ( Properties.class.isAssignableFrom( rawType ) )
{
return parseProperties( parser );
}
if ( Map.class.isAssignableFrom( rawType ) )
{
return parseMap( parser, TypeArguments.get( toType.getSupertype( Map.class ), 1 ) );
}
if ( Collection.class.isAssignableFrom( rawType ) )
{
return parseCollection( parser, TypeArguments.get( toType.getSupertype( Collection.class ), 0 ) );
}
if ( rawType.isArray() )
{
return parseArray( parser, TypeArguments.get( toType, 0 ) );
}
return parseBean( parser, toType, rawType );
}
/**
* Parses an XML subtree and converts it to the {@link Xpp3Dom} type.
*
* @param parser The XML parser
* @return Converted Xpp3Dom instance
*/
private static Xpp3Dom parseXpp3Dom( final XmlPullParser parser )
throws Exception
{
return Xpp3DomBuilder.build( parser );
}
/**
* Parses a sequence of XML elements and converts them to the appropriate {@link Properties} type.
*
* @param parser The XML parser
* @return Converted Properties instance
*/
private static Properties parseProperties( final XmlPullParser parser )
throws Exception
{
final Properties properties = newImplementation( parser, Properties.class );
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
parser.nextTag();
// 'name-then-value' or 'value-then-name'
if ( "name".equals( parser.getName() ) )
{
final String name = parser.nextText();
parser.nextTag();
properties.put( name, parser.nextText() );
}
else
{
final String value = parser.nextText();
parser.nextTag();
properties.put( parser.nextText(), value );
}
parser.nextTag();
}
return properties;
}
/**
* Parses a sequence of XML elements and converts them to the appropriate {@link Map} type.
*
* @param parser The XML parser
* @return Converted Map instance
*/
private Map<String, Object> parseMap( final MXParser parser, final TypeLiteral<?> toType )
throws Exception
{
@SuppressWarnings( "unchecked" )
final Map<String, Object> map = newImplementation( parser, HashMap.class );
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
map.put( parser.getName(), parse( parser, toType ) );
}
return map;
}
/**
* Parses a sequence of XML elements and converts them to the appropriate {@link Collection} type.
*
* @param parser The XML parser
* @return Converted Collection instance
*/
private Collection<Object> parseCollection( final MXParser parser, final TypeLiteral<?> toType )
throws Exception
{
@SuppressWarnings( "unchecked" )
final Collection<Object> collection = newImplementation( parser, ArrayList.class );
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
collection.add( parse( parser, toType ) );
}
return collection;
}
/**
* Parses a sequence of XML elements and converts them to the appropriate array type.
*
* @param parser The XML parser
* @return Converted array instance
*/
private Object parseArray( final MXParser parser, final TypeLiteral<?> toType )
throws Exception
{
// convert to a collection first then convert that into an array
final Collection<?> collection = parseCollection( parser, toType );
final Object array = Array.newInstance( toType.getRawType(), collection.size() );
int i = 0;
for ( final Object element : collection )
{
Array.set( array, i++, element );
}
return array;
}
/**
* Parses a sequence of XML elements and converts them to the appropriate bean type.
*
* @param parser The XML parser
* @return Converted bean instance
*/
private Object parseBean( final MXParser parser, final TypeLiteral<?> toType, final Class<?> rawType )
throws Exception
{
final Class<?> clazz = loadImplementation( parseImplementation( parser ), rawType );
// simple bean? assumes string constructor
if ( parser.next() == XmlPullParser.TEXT )
{
final String text = parser.getText();
// confirm element doesn't contain nested XML
if ( parser.next() != XmlPullParser.START_TAG )
{
return convertText( text, clazz == rawType ? toType : TypeLiteral.get( clazz ) );
}
}
if ( String.class == clazz )
{
// mimic plexus: discard any strings containing nested XML
while ( parser.getEventType() == XmlPullParser.START_TAG )
{
final String pos = parser.getPositionDescription();
Logs.warn( "Expected TEXT, not XML: {}", pos, new Throwable() );
parser.skipSubTree();
parser.nextTag();
}
return "";
}
final Object bean = newImplementation( clazz );
// build map of all known bean properties belonging to the chosen implementation
final Map<String, BeanProperty<Object>> propertyMap = new HashMap<String, BeanProperty<Object>>();
for ( final BeanProperty<Object> property : new BeanProperties( clazz ) )
{
final String name = property.getName();
if ( !propertyMap.containsKey( name ) )
{
propertyMap.put( name, property );
}
}
while ( parser.getEventType() == XmlPullParser.START_TAG )
{
// update properties inside the bean, guided by the cached property map
final BeanProperty<Object> property = propertyMap.get( Roles.camelizeName( parser.getName() ) );
if ( property != null )
{
property.set( bean, parse( parser, property.getType() ) );
parser.nextTag();
}
else
{
throw new XmlPullParserException( "Unknown bean property: " + parser.getName(), parser, null );
}
}
return bean;
}
/**
* Parses an XML element looking for the name of a custom implementation.
*
* @param parser The XML parser
* @return Name of the custom implementation; otherwise {@code null}
*/
private static String parseImplementation( final XmlPullParser parser )
{
return parser.getAttributeValue( null, "implementation" );
}
/**
* Attempts to load the named implementation, uses default implementation if no name is given.
*
* @param name The optional implementation name
* @param defaultClazz The default implementation type
* @return Custom implementation type if one was given; otherwise default implementation type
*/
private static Class<?> loadImplementation( final String name, final Class<?> defaultClazz )
{
if ( null == name )
{
return defaultClazz; // just use the default type
}
// TCCL allows surrounding container to influence class loading policy
final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if ( tccl != null )
{
try
{
return tccl.loadClass( name );
}
catch ( final Exception e )
{
// drop through...
}
catch ( final LinkageError e )
{
// drop through...
}
}
// assume custom type is in same class space as default
final ClassLoader peer = defaultClazz.getClassLoader();
if ( peer != null )
{
try
{
return peer.loadClass( name );
}
catch ( final Exception e )
{
// drop through...
}
catch ( final LinkageError e )
{
// drop through...
}
}
try
{
// last chance - classic model
return Class.forName( name );
}
catch ( final Exception e )
{
throw new TypeNotPresentException( name, e );
}
catch ( final LinkageError e )
{
throw new TypeNotPresentException( name, e );
}
}
/**
* Creates an instance of the given implementation using the default constructor.
*
* @param clazz The implementation type
* @return Instance of given implementation
*/
private static <T> T newImplementation( final Class<T> clazz )
{
try
{
return clazz.newInstance();
}
catch ( final Exception e )
{
throw new IllegalArgumentException( "Cannot create instance of: " + clazz, e );
}
catch ( final LinkageError e )
{
throw new IllegalArgumentException( "Cannot create instance of: " + clazz, e );
}
}
/**
* Creates an instance of the given implementation using the given string, assumes a public string constructor.
*
* @param clazz The implementation type
* @param value The string argument
* @return Instance of given implementation, constructed using the the given string
*/
private static <T> T newImplementation( final Class<T> clazz, final String value )
{
try
{
return clazz.getConstructor( String.class ).newInstance( value );
}
catch ( final Exception e )
{
final Throwable cause = e instanceof InvocationTargetException ? e.getCause() : e;
throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, clazz ), cause );
}
catch ( final LinkageError e )
{
throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, clazz ), e );
}
}
/**
* Creates an instance of the implementation named in the current XML element, or the default if no name is given.
*
* @param parser The XML parser
* @param defaultClazz The default implementation type
* @return Instance of custom implementation if one was given; otherwise instance of default type
*/
@SuppressWarnings( "unchecked" )
private static <T> T newImplementation( final XmlPullParser parser, final Class<T> defaultClazz )
{
return (T) newImplementation( loadImplementation( parseImplementation( parser ), defaultClazz ) );
}
/**
* Converts the given string to the target type, using {@link TypeConverter}s registered with the {@link Injector}.
*
* @param value The string value
* @param toType The target type
* @return Converted instance of the target type
*/
private Object convertText( final String value, final TypeLiteral<?> toType )
{
final String text = value.trim();
final Class<?> rawType = toType.getRawType();
if ( rawType.isAssignableFrom( String.class ) )
{
return text; // compatible type => no conversion needed
}
// use temporary Key as quick way to auto-box primitive types into their equivalent object types
final TypeLiteral<?> boxedType = rawType.isPrimitive() ? Key.get( rawType ).getTypeLiteral() : toType;
for ( final TypeConverterBinding b : typeConverterBindings )
{
if ( b.getTypeMatcher().matches( boxedType ) )
{
return b.getTypeConverter().convert( text, toType );
}
}
// last chance => attempt to create an instance of the expected type: use the string if non-empty
return text.length() == 0 ? newImplementation( rawType ) : newImplementation( rawType, text );
}
}