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