View Javadoc

1   /**
2    * Copyright (c) 2008-2012, http://www.snakeyaml.org
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.yaml.snakeyaml.constructor;
17  
18  import java.beans.IntrospectionException;
19  import java.math.BigDecimal;
20  import java.math.BigInteger;
21  import java.util.ArrayList;
22  import java.util.Calendar;
23  import java.util.Collection;
24  import java.util.Date;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Properties;
29  import java.util.Set;
30  import java.util.SortedMap;
31  import java.util.SortedSet;
32  import java.util.TreeMap;
33  import java.util.TreeSet;
34  
35  import org.yaml.snakeyaml.TypeDescription;
36  import org.yaml.snakeyaml.error.YAMLException;
37  import org.yaml.snakeyaml.introspector.Property;
38  import org.yaml.snakeyaml.nodes.MappingNode;
39  import org.yaml.snakeyaml.nodes.Node;
40  import org.yaml.snakeyaml.nodes.NodeId;
41  import org.yaml.snakeyaml.nodes.NodeTuple;
42  import org.yaml.snakeyaml.nodes.ScalarNode;
43  import org.yaml.snakeyaml.nodes.SequenceNode;
44  import org.yaml.snakeyaml.nodes.Tag;
45  
46  /**
47   * Construct a custom Java instance.
48   */
49  public class Constructor extends SafeConstructor {
50      private final Map<Tag, Class<? extends Object>> typeTags;
51      private final Map<Class<? extends Object>, TypeDescription> typeDefinitions;
52  
53      public Constructor() {
54          this(Object.class);
55      }
56  
57      /**
58       * Create Constructor for the specified class as the root.
59       * 
60       * @param theRoot
61       *            - the class (usually JavaBean) to be constructed
62       */
63      public Constructor(Class<? extends Object> theRoot) {
64          this(new TypeDescription(checkRoot(theRoot)));
65      }
66  
67      /**
68       * Ugly Java way to check the argument in the constructor
69       */
70      private static Class<? extends Object> checkRoot(Class<? extends Object> theRoot) {
71          if (theRoot == null) {
72              throw new NullPointerException("Root class must be provided.");
73          } else
74              return theRoot;
75      }
76  
77      public Constructor(TypeDescription theRoot) {
78          if (theRoot == null) {
79              throw new NullPointerException("Root type must be provided.");
80          }
81          this.yamlConstructors.put(null, new ConstructYamlObject());
82          if (!Object.class.equals(theRoot.getType())) {
83              rootTag = new Tag(theRoot.getType());
84          }
85          typeTags = new HashMap<Tag, Class<? extends Object>>();
86          typeDefinitions = new HashMap<Class<? extends Object>, TypeDescription>();
87          yamlClassConstructors.put(NodeId.scalar, new ConstructScalar());
88          yamlClassConstructors.put(NodeId.mapping, new ConstructMapping());
89          yamlClassConstructors.put(NodeId.sequence, new ConstructSequence());
90          addTypeDescription(theRoot);
91      }
92  
93      /**
94       * Create Constructor for a class which does not have to be in the classpath
95       * or for a definition from a Spring ApplicationContext.
96       * 
97       * @param theRoot
98       *            fully qualified class name of the root class (usually
99       *            JavaBean)
100      * @throws ClassNotFoundException
101      */
102     public Constructor(String theRoot) throws ClassNotFoundException {
103         this(Class.forName(check(theRoot)));
104     }
105 
106     private static final String check(String s) {
107         if (s == null) {
108             throw new NullPointerException("Root type must be provided.");
109         }
110         if (s.trim().length() == 0) {
111             throw new YAMLException("Root type must be provided.");
112         }
113         return s;
114     }
115 
116     /**
117      * Make YAML aware how to parse a custom Class. If there is no root Class
118      * assigned in constructor then the 'root' property of this definition is
119      * respected.
120      * 
121      * @param definition
122      *            to be added to the Constructor
123      * @return the previous value associated with <tt>definition</tt>, or
124      *         <tt>null</tt> if there was no mapping for <tt>definition</tt>.
125      */
126     public TypeDescription addTypeDescription(TypeDescription definition) {
127         if (definition == null) {
128             throw new NullPointerException("TypeDescription is required.");
129         }
130         Tag tag = definition.getTag();
131         typeTags.put(tag, definition.getType());
132         return typeDefinitions.put(definition.getType(), definition);
133     }
134 
135     /**
136      * Construct mapping instance (Map, JavaBean) when the runtime class is
137      * known.
138      */
139     protected class ConstructMapping implements Construct {
140 
141         /**
142          * Construct JavaBean. If type safe collections are used please look at
143          * <code>TypeDescription</code>.
144          * 
145          * @param node
146          *            node where the keys are property names (they can only be
147          *            <code>String</code>s) and values are objects to be created
148          * @return constructed JavaBean
149          */
150         public Object construct(Node node) {
151             MappingNode mnode = (MappingNode) node;
152             if (Properties.class.isAssignableFrom(node.getType())) {
153                 Properties properties = new Properties();
154                 if (!node.isTwoStepsConstruction()) {
155                     constructMapping2ndStep(mnode, properties);
156                 } else {
157                     throw new YAMLException("Properties must not be recursive.");
158                 }
159                 return properties;
160             } else if (SortedMap.class.isAssignableFrom(node.getType())) {
161                 SortedMap<Object, Object> map = new TreeMap<Object, Object>();
162                 if (!node.isTwoStepsConstruction()) {
163                     constructMapping2ndStep(mnode, map);
164                 }
165                 return map;
166             } else if (Map.class.isAssignableFrom(node.getType())) {
167                 if (node.isTwoStepsConstruction()) {
168                     return createDefaultMap();
169                 } else {
170                     return constructMapping(mnode);
171                 }
172             } else if (SortedSet.class.isAssignableFrom(node.getType())) {
173                 SortedSet<Object> set = new TreeSet<Object>();
174                 // XXX why this is not used ?
175                 // if (!node.isTwoStepsConstruction()) {
176                 constructSet2ndStep(mnode, set);
177                 // }
178                 return set;
179             } else if (Collection.class.isAssignableFrom(node.getType())) {
180                 if (node.isTwoStepsConstruction()) {
181                     return createDefaultSet();
182                 } else {
183                     return constructSet(mnode);
184                 }
185             } else {
186                 if (node.isTwoStepsConstruction()) {
187                     return createEmptyJavaBean(mnode);
188                 } else {
189                     return constructJavaBean2ndStep(mnode, createEmptyJavaBean(mnode));
190                 }
191             }
192         }
193 
194         @SuppressWarnings("unchecked")
195         public void construct2ndStep(Node node, Object object) {
196             if (Map.class.isAssignableFrom(node.getType())) {
197                 constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object);
198             } else if (Set.class.isAssignableFrom(node.getType())) {
199                 constructSet2ndStep((MappingNode) node, (Set<Object>) object);
200             } else {
201                 constructJavaBean2ndStep((MappingNode) node, object);
202             }
203         }
204 
205         protected Object createEmptyJavaBean(MappingNode node) {
206             try {
207                 /**
208                  * Using only default constructor. Everything else will be
209                  * initialized on 2nd step. If we do here some partial
210                  * initialization, how do we then track what need to be done on
211                  * 2nd step? I think it is better to get only object here (to
212                  * have it as reference for recursion) and do all other thing on
213                  * 2nd step.
214                  */
215                 java.lang.reflect.Constructor<?> c = node.getType().getDeclaredConstructor();
216                 c.setAccessible(true);
217                 return c.newInstance();
218             } catch (Exception e) {
219                 throw new YAMLException(e);
220             }
221         }
222 
223         protected Object constructJavaBean2ndStep(MappingNode node, Object object) {
224             flattenMapping(node);
225             Class<? extends Object> beanType = node.getType();
226             List<NodeTuple> nodeValue = node.getValue();
227             for (NodeTuple tuple : nodeValue) {
228                 ScalarNode keyNode;
229                 if (tuple.getKeyNode() instanceof ScalarNode) {
230                     // key must be scalar
231                     keyNode = (ScalarNode) tuple.getKeyNode();
232                 } else {
233                     throw new YAMLException("Keys must be scalars but found: " + tuple.getKeyNode());
234                 }
235                 Node valueNode = tuple.getValueNode();
236                 // keys can only be Strings
237                 keyNode.setType(String.class);
238                 String key = (String) constructObject(keyNode);
239                 try {
240                     Property property = getProperty(beanType, key);
241                     valueNode.setType(property.getType());
242                     TypeDescription memberDescription = typeDefinitions.get(beanType);
243                     boolean typeDetected = false;
244                     if (memberDescription != null) {
245                         switch (valueNode.getNodeId()) {
246                         case sequence:
247                             SequenceNode snode = (SequenceNode) valueNode;
248                             Class<? extends Object> memberType = memberDescription
249                                     .getListPropertyType(key);
250                             if (memberType != null) {
251                                 snode.setListType(memberType);
252                                 typeDetected = true;
253                             } else if (property.getType().isArray()) {
254                                 snode.setListType(property.getType().getComponentType());
255                                 typeDetected = true;
256                             }
257                             break;
258                         case mapping:
259                             MappingNode mnode = (MappingNode) valueNode;
260                             Class<? extends Object> keyType = memberDescription.getMapKeyType(key);
261                             if (keyType != null) {
262                                 mnode.setTypes(keyType, memberDescription.getMapValueType(key));
263                                 typeDetected = true;
264                             }
265                             break;
266                         default: // scalar
267                         }
268                     }
269                     if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) {
270                         // only if there is no explicit TypeDescription
271                         Class<?>[] arguments = property.getActualTypeArguments();
272                         if (arguments != null && arguments.length > 0) {
273                             // type safe (generic) collection may contain the
274                             // proper class
275                             if (valueNode.getNodeId() == NodeId.sequence) {
276                                 Class<?> t = arguments[0];
277                                 SequenceNode snode = (SequenceNode) valueNode;
278                                 snode.setListType(t);
279                             } else if (valueNode.getTag().equals(Tag.SET)) {
280                                 Class<?> t = arguments[0];
281                                 MappingNode mnode = (MappingNode) valueNode;
282                                 mnode.setOnlyKeyType(t);
283                                 mnode.setUseClassConstructor(true);
284                             } else if (property.getType().isAssignableFrom(Map.class)) {
285                                 Class<?> ketType = arguments[0];
286                                 Class<?> valueType = arguments[1];
287                                 MappingNode mnode = (MappingNode) valueNode;
288                                 mnode.setTypes(ketType, valueType);
289                                 mnode.setUseClassConstructor(true);
290                             } else {
291                                 // the type for collection entries cannot be
292                                 // detected
293                             }
294                         }
295                     }
296                     Object value = constructObject(valueNode);
297                     property.set(object, value);
298                 } catch (Exception e) {
299                     throw new YAMLException("Cannot create property=" + key + " for JavaBean="
300                             + object + "; " + e.getMessage(), e);
301                 }
302             }
303             return object;
304         }
305 
306         protected Property getProperty(Class<? extends Object> type, String name)
307                 throws IntrospectionException {
308             return getPropertyUtils().getProperty(type, name);
309         }
310     }
311 
312     /**
313      * Construct an instance when the runtime class is not known but a global
314      * tag with a class name is defined. It delegates the construction to the
315      * appropriate constructor based on the node kind (scalar, sequence,
316      * mapping)
317      */
318     protected class ConstructYamlObject implements Construct {
319 
320         private Construct getConstructor(Node node) {
321             Class<?> cl = getClassForNode(node);
322             node.setType(cl);
323             // call the constructor as if the runtime class is defined
324             Construct constructor = yamlClassConstructors.get(node.getNodeId());
325             return constructor;
326         }
327 
328         public Object construct(Node node) {
329             Object result = null;
330             try {
331                 result = getConstructor(node).construct(node);
332             } catch (Exception e) {
333                 throw new ConstructorException(null, null, "Can't construct a java object for "
334                         + node.getTag() + "; exception=" + e.getMessage(), node.getStartMark(), e);
335             }
336             return result;
337         }
338 
339         public void construct2ndStep(Node node, Object object) {
340             try {
341                 getConstructor(node).construct2ndStep(node, object);
342             } catch (Exception e) {
343                 throw new ConstructorException(null, null,
344                         "Can't construct a second step for a java object for " + node.getTag()
345                                 + "; exception=" + e.getMessage(), node.getStartMark(), e);
346             }
347         }
348     }
349 
350     /**
351      * Construct scalar instance when the runtime class is known. Recursive
352      * structures are not supported.
353      */
354     protected class ConstructScalar extends AbstractConstruct {
355         public Object construct(Node nnode) {
356             ScalarNode node = (ScalarNode) nnode;
357             Class<?> type = node.getType();
358             Object result;
359             if (type.isPrimitive() || type == String.class || Number.class.isAssignableFrom(type)
360                     || type == Boolean.class || Date.class.isAssignableFrom(type)
361                     || type == Character.class || type == BigInteger.class
362                     || type == BigDecimal.class || Enum.class.isAssignableFrom(type)
363                     || Tag.BINARY.equals(node.getTag()) || Calendar.class.isAssignableFrom(type)) {
364                 // standard classes created directly
365                 result = constructStandardJavaInstance(type, node);
366             } else {
367                 // there must be only 1 constructor with 1 argument
368                 java.lang.reflect.Constructor<?>[] javaConstructors = type.getConstructors();
369                 int oneArgCount = 0;
370                 java.lang.reflect.Constructor<?> javaConstructor = null;
371                 for (java.lang.reflect.Constructor<?> c : javaConstructors) {
372                     if (c.getParameterTypes().length == 1) {
373                         oneArgCount++;
374                         javaConstructor = c;
375                     }
376                 }
377                 Object argument;
378                 if (javaConstructor == null) {
379                     throw new YAMLException("No single argument constructor found for " + type);
380                 } else if (oneArgCount == 1) {
381                     argument = constructStandardJavaInstance(
382                             javaConstructor.getParameterTypes()[0], node);
383                 } else {
384                     // TODO it should be possible to use implicit types instead
385                     // of forcing String. Resolver must be available here to
386                     // obtain the implicit tag. Then we can set the tag and call
387                     // callConstructor(node) to create the argument instance.
388                     // On the other hand it may be safer to require a custom
389                     // constructor to avoid guessing the argument class
390                     argument = constructScalar(node);
391                     try {
392                         javaConstructor = type.getConstructor(String.class);
393                     } catch (Exception e) {
394                         throw new YAMLException("Can't construct a java object for scalar "
395                                 + node.getTag() + "; No String constructor found. Exception="
396                                 + e.getMessage(), e);
397                     }
398                 }
399                 try {
400                     result = javaConstructor.newInstance(argument);
401                 } catch (Exception e) {
402                     throw new ConstructorException(null, null,
403                             "Can't construct a java object for scalar " + node.getTag()
404                                     + "; exception=" + e.getMessage(), node.getStartMark(), e);
405                 }
406             }
407             return result;
408         }
409 
410         @SuppressWarnings("unchecked")
411         private Object constructStandardJavaInstance(@SuppressWarnings("rawtypes") Class type,
412                 ScalarNode node) {
413             Object result;
414             if (type == String.class) {
415                 Construct stringConstructor = yamlConstructors.get(Tag.STR);
416                 result = stringConstructor.construct(node);
417             } else if (type == Boolean.class || type == Boolean.TYPE) {
418                 Construct boolConstructor = yamlConstructors.get(Tag.BOOL);
419                 result = boolConstructor.construct(node);
420             } else if (type == Character.class || type == Character.TYPE) {
421                 Construct charConstructor = yamlConstructors.get(Tag.STR);
422                 String ch = (String) charConstructor.construct(node);
423                 if (ch.length() == 0) {
424                     result = null;
425                 } else if (ch.length() != 1) {
426                     throw new YAMLException("Invalid node Character: '" + ch + "'; length: "
427                             + ch.length());
428                 } else {
429                     result = Character.valueOf(ch.charAt(0));
430                 }
431             } else if (Date.class.isAssignableFrom(type)) {
432                 Construct dateConstructor = yamlConstructors.get(Tag.TIMESTAMP);
433                 Date date = (Date) dateConstructor.construct(node);
434                 if (type == Date.class) {
435                     result = date;
436                 } else {
437                     try {
438                         java.lang.reflect.Constructor<?> constr = type.getConstructor(long.class);
439                         result = constr.newInstance(date.getTime());
440                     } catch (Exception e) {
441                         throw new YAMLException("Cannot construct: '" + type + "'");
442                     }
443                 }
444             } else if (type == Float.class || type == Double.class || type == Float.TYPE
445                     || type == Double.TYPE || type == BigDecimal.class) {
446                 if (type == BigDecimal.class) {
447                     result = new BigDecimal(node.getValue());
448                 } else {
449                     Construct doubleConstructor = yamlConstructors.get(Tag.FLOAT);
450                     result = doubleConstructor.construct(node);
451                     if (type == Float.class || type == Float.TYPE) {
452                         result = new Float((Double) result);
453                     }
454                 }
455             } else if (type == Byte.class || type == Short.class || type == Integer.class
456                     || type == Long.class || type == BigInteger.class || type == Byte.TYPE
457                     || type == Short.TYPE || type == Integer.TYPE || type == Long.TYPE) {
458                 Construct intConstructor = yamlConstructors.get(Tag.INT);
459                 result = intConstructor.construct(node);
460                 if (type == Byte.class || type == Byte.TYPE) {
461                     result = new Byte(result.toString());
462                 } else if (type == Short.class || type == Short.TYPE) {
463                     result = new Short(result.toString());
464                 } else if (type == Integer.class || type == Integer.TYPE) {
465                     result = Integer.parseInt(result.toString());
466                 } else if (type == Long.class || type == Long.TYPE) {
467                     result = new Long(result.toString());
468                 } else {
469                     // only BigInteger left
470                     result = new BigInteger(result.toString());
471                 }
472             } else if (Enum.class.isAssignableFrom(type)) {
473                 String enumValueName = node.getValue();
474                 try {
475                     result = Enum.valueOf(type, enumValueName);
476                 } catch (Exception ex) {
477                     throw new YAMLException("Unable to find enum value '" + enumValueName
478                             + "' for enum class: " + type.getName());
479                 }
480             } else if (Calendar.class.isAssignableFrom(type)) {
481                 ConstructYamlTimestamp contr = new ConstructYamlTimestamp();
482                 contr.construct(node);
483                 result = contr.getCalendar();
484             } else {
485                 throw new YAMLException("Unsupported class: " + type);
486             }
487             return result;
488         }
489     }
490 
491     /**
492      * Construct sequence (List, Array, or immutable object) when the runtime
493      * class is known.
494      */
495     protected class ConstructSequence implements Construct {
496         @SuppressWarnings("unchecked")
497         public Object construct(Node node) {
498             SequenceNode snode = (SequenceNode) node;
499             if (Set.class.isAssignableFrom(node.getType())) {
500                 if (node.isTwoStepsConstruction()) {
501                     throw new YAMLException("Set cannot be recursive.");
502                 } else {
503                     return constructSet(snode);
504                 }
505             } else if (Collection.class.isAssignableFrom(node.getType())) {
506                 if (node.isTwoStepsConstruction()) {
507                     return createDefaultList(snode.getValue().size());
508                 } else {
509                     return constructSequence(snode);
510                 }
511             } else if (node.getType().isArray()) {
512                 if (node.isTwoStepsConstruction()) {
513                     return createArray(node.getType(), snode.getValue().size());
514                 } else {
515                     return constructArray(snode);
516                 }
517             } else {
518                 // create immutable object
519                 List<java.lang.reflect.Constructor<?>> possibleConstructors = new ArrayList<java.lang.reflect.Constructor<?>>(
520                         snode.getValue().size());
521                 for (java.lang.reflect.Constructor<?> constructor : node.getType()
522                         .getConstructors()) {
523                     if (snode.getValue().size() == constructor.getParameterTypes().length) {
524                         possibleConstructors.add(constructor);
525                     }
526                 }
527                 if (!possibleConstructors.isEmpty()) {
528                     if (possibleConstructors.size() == 1) {
529                         Object[] argumentList = new Object[snode.getValue().size()];
530                         java.lang.reflect.Constructor<?> c = possibleConstructors.get(0);
531                         int index = 0;
532                         for (Node argumentNode : snode.getValue()) {
533                             Class<?> type = c.getParameterTypes()[index];
534                             // set runtime classes for arguments
535                             argumentNode.setType(type);
536                             argumentList[index++] = constructObject(argumentNode);
537                         }
538 
539                         try {
540                             return c.newInstance(argumentList);
541                         } catch (Exception e) {
542                             throw new YAMLException(e);
543                         }
544                     }
545 
546                     // use BaseConstructor
547                     List<Object> argumentList = (List<Object>) constructSequence(snode);
548                     Class<?>[] parameterTypes = new Class[argumentList.size()];
549                     int index = 0;
550                     for (Object parameter : argumentList) {
551                         parameterTypes[index] = parameter.getClass();
552                         index++;
553                     }
554 
555                     for (java.lang.reflect.Constructor<?> c : possibleConstructors) {
556                         Class<?>[] argTypes = c.getParameterTypes();
557                         boolean foundConstructor = true;
558                         for (int i = 0; i < argTypes.length; i++) {
559                             if (!wrapIfPrimitive(argTypes[i]).isAssignableFrom(parameterTypes[i])) {
560                                 foundConstructor = false;
561                                 break;
562                             }
563                         }
564                         if (foundConstructor) {
565                             try {
566                                 return c.newInstance(argumentList.toArray());
567                             } catch (Exception e) {
568                                 throw new YAMLException(e);
569                             }
570                         }
571                     }
572                 }
573                 throw new YAMLException("No suitable constructor with "
574                         + String.valueOf(snode.getValue().size()) + " arguments found for "
575                         + node.getType());
576 
577             }
578         }
579 
580         private final Class<? extends Object> wrapIfPrimitive(Class<?> clazz) {
581             if (!clazz.isPrimitive()) {
582                 return clazz;
583             }
584             if (clazz == Integer.TYPE) {
585                 return Integer.class;
586             }
587             if (clazz == Float.TYPE) {
588                 return Float.class;
589             }
590             if (clazz == Double.TYPE) {
591                 return Double.class;
592             }
593             if (clazz == Boolean.TYPE) {
594                 return Boolean.class;
595             }
596             if (clazz == Long.TYPE) {
597                 return Long.class;
598             }
599             if (clazz == Character.TYPE) {
600                 return Character.class;
601             }
602             if (clazz == Short.TYPE) {
603                 return Short.class;
604             }
605             if (clazz == Byte.TYPE) {
606                 return Byte.class;
607             }
608             throw new YAMLException("Unexpected primitive " + clazz);
609         }
610 
611         @SuppressWarnings("unchecked")
612         public void construct2ndStep(Node node, Object object) {
613             SequenceNode snode = (SequenceNode) node;
614             if (List.class.isAssignableFrom(node.getType())) {
615                 List<Object> list = (List<Object>) object;
616                 constructSequenceStep2(snode, list);
617             } else if (node.getType().isArray()) {
618                 constructArrayStep2(snode, object);
619             } else {
620                 throw new YAMLException("Immutable objects cannot be recursive.");
621             }
622         }
623     }
624 
625     protected Class<?> getClassForNode(Node node) {
626         Class<? extends Object> classForTag = typeTags.get(node.getTag());
627         if (classForTag == null) {
628             String name = node.getTag().getClassName();
629             Class<?> cl;
630             try {
631                 cl = getClassForName(name);
632             } catch (ClassNotFoundException e) {
633                 throw new YAMLException("Class not found: " + name);
634             }
635             typeTags.put(node.getTag(), cl);
636             return cl;
637         } else {
638             return classForTag;
639         }
640     }
641 
642     protected Class<?> getClassForName(String name) throws ClassNotFoundException {
643         return Class.forName(name);
644     }
645 }