View Javadoc
1   package org.eclipse.sisu.plexus;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import javax.annotation.Priority;
23  import javax.inject.Inject;
24  import javax.inject.Singleton;
25  
26  import java.io.StringReader;
27  import java.lang.reflect.Array;
28  import java.lang.reflect.InvocationTargetException;
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.HashMap;
32  import java.util.Map;
33  import java.util.Properties;
34  
35  import com.google.inject.Injector;
36  import com.google.inject.Key;
37  import com.google.inject.Module;
38  import com.google.inject.TypeLiteral;
39  import com.google.inject.spi.TypeConverter;
40  import com.google.inject.spi.TypeConverterBinding;
41  import org.apache.maven.api.xml.Dom;
42  import org.codehaus.plexus.util.xml.Xpp3Dom;
43  import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
44  import org.codehaus.plexus.util.xml.pull.MXParser;
45  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
46  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
47  import org.eclipse.sisu.bean.BeanProperties;
48  import org.eclipse.sisu.bean.BeanProperty;
49  import org.eclipse.sisu.inject.Logs;
50  import org.eclipse.sisu.inject.TypeArguments;
51  
52  /**
53   * {@link PlexusBeanConverter} {@link Module} that converts Plexus XML configuration into beans.
54   */
55  @Singleton
56  @Priority( 10 )
57  public final class PlexusXmlBeanConverter
58      implements PlexusBeanConverter
59  {
60      // ----------------------------------------------------------------------
61      // Constants
62      // ----------------------------------------------------------------------
63  
64      private static final String CONVERSION_ERROR = "Cannot convert: \"%s\" to: %s";
65  
66      // ----------------------------------------------------------------------
67      // Implementation fields
68      // ----------------------------------------------------------------------
69  
70      private final Collection<TypeConverterBinding> typeConverterBindings;
71  
72      // ----------------------------------------------------------------------
73      // Constructors
74      // ----------------------------------------------------------------------
75  
76      @Inject
77      PlexusXmlBeanConverter( final Injector injector )
78      {
79          typeConverterBindings = injector.getTypeConverterBindings();
80      }
81  
82      // ----------------------------------------------------------------------
83      // Public methods
84      // ----------------------------------------------------------------------
85  
86      @SuppressWarnings( { "unchecked", "rawtypes" } )
87      public Object convert( final TypeLiteral role, final String value )
88      {
89          if ( value.trim().startsWith( "<" ) )
90          {
91              try
92              {
93                  final MXParser parser = new MXParser();
94                  parser.setInput( new StringReader( value ) );
95                  parser.nextTag();
96  
97                  return parse( parser, role );
98              }
99              catch ( final Exception e )
100             {
101                 throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, role ), e );
102             }
103         }
104 
105         return convertText( value, role );
106     }
107 
108     // ----------------------------------------------------------------------
109     // Implementation methods
110     // ----------------------------------------------------------------------
111 
112     /**
113      * Parses a sequence of XML elements and converts them to the given target type.
114      * 
115      * @param parser The XML parser
116      * @param toType The target type
117      * @return Converted instance of the target type
118      */
119     private Object parse( final MXParser parser, final TypeLiteral<?> toType )
120         throws Exception
121     {
122         parser.require( XmlPullParser.START_TAG, null, null );
123 
124         final Class<?> rawType = toType.getRawType();
125         if ( Dom.class.isAssignableFrom( rawType ) )
126         {
127             return org.apache.maven.internal.xml.Xpp3DomBuilder.build( parser );
128         }
129         if ( Xpp3Dom.class.isAssignableFrom( rawType ) )
130         {
131             return parseXpp3Dom( parser );
132         }
133         if ( Properties.class.isAssignableFrom( rawType ) )
134         {
135             return parseProperties( parser );
136         }
137         if ( Map.class.isAssignableFrom( rawType ) )
138         {
139             return parseMap( parser, TypeArguments.get( toType.getSupertype( Map.class ), 1 ) );
140         }
141         if ( Collection.class.isAssignableFrom( rawType ) )
142         {
143             return parseCollection( parser, TypeArguments.get( toType.getSupertype( Collection.class ), 0 ) );
144         }
145         if ( rawType.isArray() )
146         {
147             return parseArray( parser, TypeArguments.get( toType, 0 ) );
148         }
149         return parseBean( parser, toType, rawType );
150     }
151 
152     /**
153      * Parses an XML subtree and converts it to the {@link Xpp3Dom} type.
154      * 
155      * @param parser The XML parser
156      * @return Converted Xpp3Dom instance
157      */
158     private static Xpp3Dom parseXpp3Dom( final XmlPullParser parser )
159         throws Exception
160     {
161         return Xpp3DomBuilder.build( parser );
162     }
163 
164     /**
165      * Parses a sequence of XML elements and converts them to the appropriate {@link Properties} type.
166      * 
167      * @param parser The XML parser
168      * @return Converted Properties instance
169      */
170     private static Properties parseProperties( final XmlPullParser parser )
171         throws Exception
172     {
173         final Properties properties = newImplementation( parser, Properties.class );
174         while ( parser.nextTag() == XmlPullParser.START_TAG )
175         {
176             parser.nextTag();
177             // 'name-then-value' or 'value-then-name'
178             if ( "name".equals( parser.getName() ) )
179             {
180                 final String name = parser.nextText();
181                 parser.nextTag();
182                 properties.put( name, parser.nextText() );
183             }
184             else
185             {
186                 final String value = parser.nextText();
187                 parser.nextTag();
188                 properties.put( parser.nextText(), value );
189             }
190             parser.nextTag();
191         }
192         return properties;
193     }
194 
195     /**
196      * Parses a sequence of XML elements and converts them to the appropriate {@link Map} type.
197      * 
198      * @param parser The XML parser
199      * @return Converted Map instance
200      */
201     private Map<String, Object> parseMap( final MXParser parser, final TypeLiteral<?> toType )
202         throws Exception
203     {
204         @SuppressWarnings( "unchecked" )
205         final Map<String, Object> map = newImplementation( parser, HashMap.class );
206         while ( parser.nextTag() == XmlPullParser.START_TAG )
207         {
208             map.put( parser.getName(), parse( parser, toType ) );
209         }
210         return map;
211     }
212 
213     /**
214      * Parses a sequence of XML elements and converts them to the appropriate {@link Collection} type.
215      * 
216      * @param parser The XML parser
217      * @return Converted Collection instance
218      */
219     private Collection<Object> parseCollection( final MXParser parser, final TypeLiteral<?> toType )
220         throws Exception
221     {
222         @SuppressWarnings( "unchecked" )
223         final Collection<Object> collection = newImplementation( parser, ArrayList.class );
224         while ( parser.nextTag() == XmlPullParser.START_TAG )
225         {
226             collection.add( parse( parser, toType ) );
227         }
228         return collection;
229     }
230 
231     /**
232      * Parses a sequence of XML elements and converts them to the appropriate array type.
233      * 
234      * @param parser The XML parser
235      * @return Converted array instance
236      */
237     private Object parseArray( final MXParser parser, final TypeLiteral<?> toType )
238         throws Exception
239     {
240         // convert to a collection first then convert that into an array
241         final Collection<?> collection = parseCollection( parser, toType );
242         final Object array = Array.newInstance( toType.getRawType(), collection.size() );
243 
244         int i = 0;
245         for ( final Object element : collection )
246         {
247             Array.set( array, i++, element );
248         }
249 
250         return array;
251     }
252 
253     /**
254      * Parses a sequence of XML elements and converts them to the appropriate bean type.
255      * 
256      * @param parser The XML parser
257      * @return Converted bean instance
258      */
259     private Object parseBean( final MXParser parser, final TypeLiteral<?> toType, final Class<?> rawType )
260         throws Exception
261     {
262         final Class<?> clazz = loadImplementation( parseImplementation( parser ), rawType );
263 
264         // simple bean? assumes string constructor
265         if ( parser.next() == XmlPullParser.TEXT )
266         {
267             final String text = parser.getText();
268 
269             // confirm element doesn't contain nested XML
270             if ( parser.next() != XmlPullParser.START_TAG )
271             {
272                 return convertText( text, clazz == rawType ? toType : TypeLiteral.get( clazz ) );
273             }
274         }
275 
276         if ( String.class == clazz )
277         {
278             // mimic plexus: discard any strings containing nested XML
279             while ( parser.getEventType() == XmlPullParser.START_TAG )
280             {
281                 final String pos = parser.getPositionDescription();
282                 Logs.warn( "Expected TEXT, not XML: {}", pos, new Throwable() );
283                 parser.skipSubTree();
284                 parser.nextTag();
285             }
286             return "";
287         }
288 
289         final Object bean = newImplementation( clazz );
290 
291         // build map of all known bean properties belonging to the chosen implementation
292         final Map<String, BeanProperty<Object>> propertyMap = new HashMap<String, BeanProperty<Object>>();
293         for ( final BeanProperty<Object> property : new BeanProperties( clazz ) )
294         {
295             final String name = property.getName();
296             if ( !propertyMap.containsKey( name ) )
297             {
298                 propertyMap.put( name, property );
299             }
300         }
301 
302         while ( parser.getEventType() == XmlPullParser.START_TAG )
303         {
304             // update properties inside the bean, guided by the cached property map
305             final BeanProperty<Object> property = propertyMap.get( Roles.camelizeName( parser.getName() ) );
306             if ( property != null )
307             {
308                 property.set( bean, parse( parser, property.getType() ) );
309                 parser.nextTag();
310             }
311             else
312             {
313                 throw new XmlPullParserException( "Unknown bean property: " + parser.getName(), parser, null );
314             }
315         }
316 
317         return bean;
318     }
319 
320     /**
321      * Parses an XML element looking for the name of a custom implementation.
322      * 
323      * @param parser The XML parser
324      * @return Name of the custom implementation; otherwise {@code null}
325      */
326     private static String parseImplementation( final XmlPullParser parser )
327     {
328         return parser.getAttributeValue( null, "implementation" );
329     }
330 
331     /**
332      * Attempts to load the named implementation, uses default implementation if no name is given.
333      * 
334      * @param name The optional implementation name
335      * @param defaultClazz The default implementation type
336      * @return Custom implementation type if one was given; otherwise default implementation type
337      */
338     private static Class<?> loadImplementation( final String name, final Class<?> defaultClazz )
339     {
340         if ( null == name )
341         {
342             return defaultClazz; // just use the default type
343         }
344 
345         // TCCL allows surrounding container to influence class loading policy
346         final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
347         if ( tccl != null )
348         {
349             try
350             {
351                 return tccl.loadClass( name );
352             }
353             catch ( final Exception e )
354             {
355                 // drop through...
356             }
357             catch ( final LinkageError e )
358             {
359                 // drop through...
360             }
361         }
362 
363         // assume custom type is in same class space as default
364         final ClassLoader peer = defaultClazz.getClassLoader();
365         if ( peer != null )
366         {
367             try
368             {
369                 return peer.loadClass( name );
370             }
371             catch ( final Exception e )
372             {
373                 // drop through...
374             }
375             catch ( final LinkageError e )
376             {
377                 // drop through...
378             }
379         }
380 
381         try
382         {
383             // last chance - classic model
384             return Class.forName( name );
385         }
386         catch ( final Exception e )
387         {
388             throw new TypeNotPresentException( name, e );
389         }
390         catch ( final LinkageError e )
391         {
392             throw new TypeNotPresentException( name, e );
393         }
394     }
395 
396     /**
397      * Creates an instance of the given implementation using the default constructor.
398      * 
399      * @param clazz The implementation type
400      * @return Instance of given implementation
401      */
402     private static <T> T newImplementation( final Class<T> clazz )
403     {
404         try
405         {
406             return clazz.newInstance();
407         }
408         catch ( final Exception e )
409         {
410             throw new IllegalArgumentException( "Cannot create instance of: " + clazz, e );
411         }
412         catch ( final LinkageError e )
413         {
414             throw new IllegalArgumentException( "Cannot create instance of: " + clazz, e );
415         }
416     }
417 
418     /**
419      * Creates an instance of the given implementation using the given string, assumes a public string constructor.
420      * 
421      * @param clazz The implementation type
422      * @param value The string argument
423      * @return Instance of given implementation, constructed using the the given string
424      */
425     private static <T> T newImplementation( final Class<T> clazz, final String value )
426     {
427         try
428         {
429             return clazz.getConstructor( String.class ).newInstance( value );
430         }
431         catch ( final Exception e )
432         {
433             final Throwable cause = e instanceof InvocationTargetException ? e.getCause() : e;
434             throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, clazz ), cause );
435         }
436         catch ( final LinkageError e )
437         {
438             throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, clazz ), e );
439         }
440     }
441 
442     /**
443      * Creates an instance of the implementation named in the current XML element, or the default if no name is given.
444      * 
445      * @param parser The XML parser
446      * @param defaultClazz The default implementation type
447      * @return Instance of custom implementation if one was given; otherwise instance of default type
448      */
449     @SuppressWarnings( "unchecked" )
450     private static <T> T newImplementation( final XmlPullParser parser, final Class<T> defaultClazz )
451     {
452         return (T) newImplementation( loadImplementation( parseImplementation( parser ), defaultClazz ) );
453     }
454 
455     /**
456      * Converts the given string to the target type, using {@link TypeConverter}s registered with the {@link Injector}.
457      * 
458      * @param value The string value
459      * @param toType The target type
460      * @return Converted instance of the target type
461      */
462     private Object convertText( final String value, final TypeLiteral<?> toType )
463     {
464         final String text = value.trim();
465 
466         final Class<?> rawType = toType.getRawType();
467         if ( rawType.isAssignableFrom( String.class ) )
468         {
469             return text; // compatible type => no conversion needed
470         }
471 
472         // use temporary Key as quick way to auto-box primitive types into their equivalent object types
473         final TypeLiteral<?> boxedType = rawType.isPrimitive() ? Key.get( rawType ).getTypeLiteral() : toType;
474 
475         for ( final TypeConverterBinding b : typeConverterBindings )
476         {
477             if ( b.getTypeMatcher().matches( boxedType ) )
478             {
479                 return b.getTypeConverter().convert( text, toType );
480             }
481         }
482 
483         // last chance => attempt to create an instance of the expected type: use the string if non-empty
484         return text.length() == 0 ? newImplementation( rawType ) : newImplementation( rawType, text );
485     }
486 }