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