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