View Javadoc

1   /**
2    * Copyright (c) 2008-2011, 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  
17  package org.yaml.snakeyaml.constructor;
18  
19  import java.beans.IntrospectionException;
20  import java.math.BigDecimal;
21  import java.math.BigInteger;
22  import java.util.ArrayList;
23  import java.util.Calendar;
24  import java.util.Collection;
25  import java.util.Date;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Properties;
30  import java.util.Set;
31  import java.util.SortedMap;
32  import java.util.SortedSet;
33  import java.util.TreeMap;
34  import java.util.TreeSet;
35  
36  import org.yaml.snakeyaml.TypeDescription;
37  import org.yaml.snakeyaml.error.YAMLException;
38  import org.yaml.snakeyaml.introspector.Property;
39  import org.yaml.snakeyaml.nodes.MappingNode;
40  import org.yaml.snakeyaml.nodes.Node;
41  import org.yaml.snakeyaml.nodes.NodeId;
42  import org.yaml.snakeyaml.nodes.NodeTuple;
43  import org.yaml.snakeyaml.nodes.ScalarNode;
44  import org.yaml.snakeyaml.nodes.SequenceNode;
45  import org.yaml.snakeyaml.nodes.Tag;
46  
47  /**
48   * Construct a custom Java instance.
49   */
50  public class Constructor extends SafeConstructor {
51      private final Map<Tag, Class<? extends Object>> typeTags;
52      private final Map<Class<? extends Object>, TypeDescription> typeDefinitions;
53  
54      public Constructor() {
55          this(Object.class);
56      }
57  
58      /**
59       * Create Constructor for the specified class as the root.
60       * 
61       * @param theRoot
62       *            - the class (usually JavaBean) to be constructed
63       */
64      public Constructor(Class<? extends Object> theRoot) {
65          this(new TypeDescription(checkRoot(theRoot)));
66      }
67  
68      /**
69       * Ugly Java way to check the argument in the constructor
70       */
71      private static Class<? extends Object> checkRoot(Class<? extends Object> theRoot) {
72          if (theRoot == null) {
73              throw new NullPointerException("Root class must be provided.");
74          } else
75              return theRoot;
76      }
77  
78      public Constructor(TypeDescription theRoot) {
79          if (theRoot == null) {
80              throw new NullPointerException("Root type must be provided.");
81          }
82          this.yamlConstructors.put(null, new ConstructYamlObject());
83          if (!Object.class.equals(theRoot.getType())) {
84              rootTag = new Tag(theRoot.getType());
85          }
86          typeTags = new HashMap<Tag, Class<? extends Object>>();
87          typeDefinitions = new HashMap<Class<? extends Object>, TypeDescription>();
88          yamlClassConstructors.put(NodeId.scalar, new ConstructScalar());
89          yamlClassConstructors.put(NodeId.mapping, new ConstructMapping());
90          yamlClassConstructors.put(NodeId.sequence, new ConstructSequence());
91          addTypeDescription(theRoot);
92      }
93  
94      /**
95       * Create Constructor for a class which does not have to be in the classpath
96       * or for a definition from a Spring ApplicationContext.
97       * 
98       * @param theRoot
99       *            fully qualified class name of the root class (usually
100      *            JavaBean)
101      * @throws ClassNotFoundException
102      */
103     public Constructor(String theRoot) throws ClassNotFoundException {
104         this(Class.forName(check(theRoot)));
105     }
106 
107     private static final String check(String s) {
108         if (s == null) {
109             throw new NullPointerException("Root type must be provided.");
110         }
111         if (s.trim().length() == 0) {
112             throw new YAMLException("Root type must be provided.");
113         }
114         return s;
115     }
116 
117     /**
118      * Make YAML aware how to parse a custom Class. If there is no root Class
119      * assigned in constructor then the 'root' property of this definition is
120      * respected.
121      * 
122      * @param definition
123      *            to be added to the Constructor
124      * @return the previous value associated with <tt>definition</tt>, or
125      *         <tt>null</tt> if there was no mapping for <tt>definition</tt>.
126      */
127     public TypeDescription addTypeDescription(TypeDescription definition) {
128         if (definition == null) {
129             throw new NullPointerException("TypeDescription is required.");
130         }
131         Tag tag = definition.getTag();
132         typeTags.put(tag, definition.getType());
133         return typeDefinitions.put(definition.getType(), definition);
134     }
135 
136     /**
137      * Construct mapping instance (Map, JavaBean) when the runtime class is
138      * known.
139      */
140     protected class ConstructMapping implements Construct {
141 
142         /**
143          * Construct JavaBean. If type safe collections are used please look at
144          * <code>TypeDescription</code>.
145          * 
146          * @param node
147          *            node where the keys are property names (they can only be
148          *            <code>String</code>s) and values are objects to be created
149          * @return constructed JavaBean
150          */
151         public Object construct(Node node) {
152             MappingNode mnode = (MappingNode) node;
153             if (Properties.class.isAssignableFrom(node.getType())) {
154                 Properties properties = new Properties();
155                 if (!node.isTwoStepsConstruction()) {
156                     constructMapping2ndStep(mnode, properties);
157                 } else {
158                     throw new YAMLException("Properties must not be recursive.");
159                 }
160                 return properties;
161             } else if (SortedMap.class.isAssignableFrom(node.getType())) {
162                 SortedMap<Object, Object> map = new TreeMap<Object, Object>();
163                 if (!node.isTwoStepsConstruction()) {
164                     constructMapping2ndStep(mnode, map);
165                 }
166                 return map;
167             } else if (Map.class.isAssignableFrom(node.getType())) {
168                 if (node.isTwoStepsConstruction()) {
169                     return createDefaultMap();
170                 } else {
171                     return constructMapping(mnode);
172                 }
173             } else if (SortedSet.class.isAssignableFrom(node.getType())) {
174                 SortedSet<Object> set = new TreeSet<Object>();
175                 // XXX why this is not used ?
176                 // if (!node.isTwoStepsConstruction()) {
177                 constructSet2ndStep(mnode, set);
178                 // }
179                 return set;
180             } else if (Collection.class.isAssignableFrom(node.getType())) {
181                 if (node.isTwoStepsConstruction()) {
182                     return createDefaultSet();
183                 } else {
184                     return constructSet(mnode);
185                 }
186             } else {
187                 if (node.isTwoStepsConstruction()) {
188                     return createEmptyJavaBean(mnode);
189                 } else {
190                     return constructJavaBean2ndStep(mnode, createEmptyJavaBean(mnode));
191                 }
192             }
193         }
194 
195         @SuppressWarnings("unchecked")
196         public void construct2ndStep(Node node, Object object) {
197             if (Map.class.isAssignableFrom(node.getType())) {
198                 constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object);
199             } else if (Set.class.isAssignableFrom(node.getType())) {
200                 constructSet2ndStep((MappingNode) node, (Set<Object>) object);
201             } else {
202                 constructJavaBean2ndStep((MappingNode) node, object);
203             }
204         }
205 
206         protected Object createEmptyJavaBean(MappingNode node) {
207             try {
208                 /**
209                  * Using only default constructor. Everything else will be
210                  * initialized on 2nd step. If we do here some partial
211                  * initialization, how do we then track what need to be done on
212                  * 2nd step? I think it is better to get only object here (to
213                  * have it as reference for recursion) and do all other thing on
214                  * 2nd step.
215                  */
216                 java.lang.reflect.Constructor<?> c = node.getType().getDeclaredConstructor();
217                 c.setAccessible(true);
218                 return c.newInstance();
219             } catch (Exception e) {
220                 throw new YAMLException(e);
221             }
222         }
223 
224         protected Object constructJavaBean2ndStep(MappingNode node, Object object) {
225             flattenMapping(node);
226             Class<? extends Object> beanType = node.getType();
227             List<NodeTuple> nodeValue = node.getValue();
228             for (NodeTuple tuple : nodeValue) {
229                 ScalarNode keyNode;
230                 if (tuple.getKeyNode() instanceof ScalarNode) {
231                     // key must be scalar
232                     keyNode = (ScalarNode) tuple.getKeyNode();
233                 } else {
234                     throw new YAMLException("Keys must be scalars but found: " + tuple.getKeyNode());
235                 }
236                 Node valueNode = tuple.getValueNode();
237                 // keys can only be Strings
238                 keyNode.setType(String.class);
239                 String key = (String) constructObject(keyNode);
240                 try {
241                     Property property = getProperty(beanType, key);
242                     valueNode.setType(property.getType());
243                     TypeDescription memberDescription = typeDefinitions.get(beanType);
244                     boolean typeDetected = false;
245                     if (memberDescription != null) {
246                         switch (valueNode.getNodeId()) {
247                         case sequence:
248                             SequenceNode snode = (SequenceNode) valueNode;
249                             Class<? extends Object> memberType = memberDescription
250                                     .getListPropertyType(key);
251                             if (memberType != null) {
252                                 snode.setListType(memberType);
253                                 typeDetected = true;
254                             } else if (property.getType().isArray()) {
255                                 snode.setListType(property.getType().getComponentType());
256                                 typeDetected = true;
257                             }
258                             break;
259                         case mapping:
260                             MappingNode mnode = (MappingNode) valueNode;
261                             Class<? extends Object> keyType = memberDescription.getMapKeyType(key);
262                             if (keyType != null) {
263                                 mnode.setTypes(keyType, memberDescription.getMapValueType(key));
264                                 typeDetected = true;
265                             }
266                             break;
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) {
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 ConstructorException(null, null,
395                                 "Can't construct a java object for scalar " + node.getTag()
396                                         + "; No String constructor found. Exception="
397                                         + e.getMessage(), node.getStartMark(), e);
398                     }
399                 }
400                 try {
401                     result = javaConstructor.newInstance(argument);
402                 } catch (Exception e) {
403                     throw new ConstructorException(null, null,
404                             "Can't construct a java object for scalar " + node.getTag()
405                                     + "; exception=" + e.getMessage(), node.getStartMark(), e);
406                 }
407             }
408             return result;
409         }
410 
411         @SuppressWarnings("unchecked")
412         private Object constructStandardJavaInstance(@SuppressWarnings("rawtypes") Class type,
413                 ScalarNode node) {
414             Object result;
415             if (type == String.class) {
416                 Construct stringConstructor = yamlConstructors.get(Tag.STR);
417                 result = stringConstructor.construct(node);
418             } else if (type == Boolean.class || type == Boolean.TYPE) {
419                 Construct boolConstructor = yamlConstructors.get(Tag.BOOL);
420                 result = boolConstructor.construct(node);
421             } else if (type == Character.class || type == Character.TYPE) {
422                 Construct charConstructor = yamlConstructors.get(Tag.STR);
423                 String ch = (String) charConstructor.construct(node);
424                 if (ch.length() == 0) {
425                     result = null;
426                 } else if (ch.length() != 1) {
427                     throw new YAMLException("Invalid node Character: '" + ch + "'; length: "
428                             + ch.length());
429                 } else {
430                     result = new Character(ch.charAt(0));
431                 }
432             } else if (Date.class.isAssignableFrom(type)) {
433                 Construct dateConstructor = yamlConstructors.get(Tag.TIMESTAMP);
434                 Date date = (Date) dateConstructor.construct(node);
435                 if (type == Date.class) {
436                     result = date;
437                 } else {
438                     try {
439                         java.lang.reflect.Constructor<?> constr = type.getConstructor(long.class);
440                         result = constr.newInstance(date.getTime());
441                     } catch (Exception e) {
442                         throw new YAMLException("Cannot construct: '" + type + "'");
443                     }
444                 }
445             } else if (type == Float.class || type == Double.class || type == Float.TYPE
446                     || type == Double.TYPE || type == BigDecimal.class) {
447                 if (type == BigDecimal.class) {
448                     result = new BigDecimal(node.getValue());
449                 } else {
450                     Construct doubleConstructor = yamlConstructors.get(Tag.FLOAT);
451                     result = doubleConstructor.construct(node);
452                     if (type == Float.class || type == Float.TYPE) {
453                         result = new Float((Double) result);
454                     }
455                 }
456             } else if (type == Byte.class || type == Short.class || type == Integer.class
457                     || type == Long.class || type == BigInteger.class || type == Byte.TYPE
458                     || type == Short.TYPE || type == Integer.TYPE || type == Long.TYPE) {
459                 Construct intConstructor = yamlConstructors.get(Tag.INT);
460                 result = intConstructor.construct(node);
461                 if (type == Byte.class || type == Byte.TYPE) {
462                     result = new Byte(result.toString());
463                 } else if (type == Short.class || type == Short.TYPE) {
464                     result = new Short(result.toString());
465                 } else if (type == Integer.class || type == Integer.TYPE) {
466                     result = new Integer(result.toString());
467                 } else if (type == Long.class || type == Long.TYPE) {
468                     result = new Long(result.toString());
469                 } else {
470                     // only BigInteger left
471                     result = new BigInteger(result.toString());
472                 }
473             } else if (Enum.class.isAssignableFrom(type)) {
474                 String enumValueName = node.getValue();
475                 try {
476                     result = Enum.valueOf(type, enumValueName);
477                 } catch (Exception ex) {
478                     throw new YAMLException("Unable to find enum value '" + enumValueName
479                             + "' for enum class: " + type.getName());
480                 }
481             } else if (Calendar.class.isAssignableFrom(type)) {
482                 ConstructYamlTimestamp contr = new ConstructYamlTimestamp();
483                 contr.construct(node);
484                 result = contr.getCalendar();
485             } else {
486                 throw new YAMLException("Unsupported class: " + type);
487             }
488             return result;
489         }
490     }
491 
492     /**
493      * Construct sequence (List, Array, or immutable object) when the runtime
494      * class is known.
495      */
496     protected class ConstructSequence implements Construct {
497         @SuppressWarnings("unchecked")
498         public Object construct(Node node) {
499             SequenceNode snode = (SequenceNode) node;
500             if (Set.class.isAssignableFrom(node.getType())) {
501                 if (node.isTwoStepsConstruction()) {
502                     throw new YAMLException("Set cannot be recursive.");
503                 } else {
504                     return constructSet(snode);
505                 }
506             } else if (Collection.class.isAssignableFrom(node.getType())) {
507                 if (node.isTwoStepsConstruction()) {
508                     return createDefaultList(snode.getValue().size());
509                 } else {
510                     return constructSequence(snode);
511                 }
512             } else if (node.getType().isArray()) {
513                 if (node.isTwoStepsConstruction()) {
514                     return createArray(node.getType(), snode.getValue().size());
515                 } else {
516                     return constructArray(snode);
517                 }
518             } else {
519                 // create immutable object
520                 List<java.lang.reflect.Constructor<?>> possibleConstructors = new ArrayList<java.lang.reflect.Constructor<?>>(
521                         snode.getValue().size());
522                 for (java.lang.reflect.Constructor<?> constructor : node.getType()
523                         .getConstructors()) {
524                     if (snode.getValue().size() == constructor.getParameterTypes().length) {
525                         possibleConstructors.add(constructor);
526                     }
527                 }
528                 if (!possibleConstructors.isEmpty()) {
529                     if (possibleConstructors.size() == 1) {
530                         Object[] argumentList = new Object[snode.getValue().size()];
531                         java.lang.reflect.Constructor<?> c = possibleConstructors.get(0);
532                         int index = 0;
533                         for (Node argumentNode : snode.getValue()) {
534                             Class<?> type = c.getParameterTypes()[index];
535                             // set runtime classes for arguments
536                             argumentNode.setType(type);
537                             argumentList[index++] = constructObject(argumentNode);
538                         }
539 
540                         try {
541                             return c.newInstance(argumentList);
542                         } catch (Exception e) {
543                             throw new YAMLException(e);
544                         }
545                     }
546 
547                     // use BaseConstructor
548                     List<Object> argumentList = (List<Object>) constructSequence(snode);
549                     Class<?>[] parameterTypes = new Class[argumentList.size()];
550                     int index = 0;
551                     for (Object parameter : argumentList) {
552                         parameterTypes[index] = parameter.getClass();
553                         index++;
554                     }
555 
556                     for (java.lang.reflect.Constructor<?> c : possibleConstructors) {
557                         Class<?>[] argTypes = c.getParameterTypes();
558                         boolean foundConstructor = true;
559                         for (int i = 0; i < argTypes.length; i++) {
560                             if (!wrapIfPrimitive(argTypes[i]).isAssignableFrom(parameterTypes[i])) {
561                                 foundConstructor = false;
562                                 break;
563                             }
564                         }
565                         if (foundConstructor) {
566                             try {
567                                 return c.newInstance(argumentList.toArray());
568                             } catch (Exception e) {
569                                 throw new YAMLException(e);
570                             }
571                         }
572                     }
573                 }
574                 throw new YAMLException("No suitable constructor with "
575                         + String.valueOf(snode.getValue().size()) + " arguments found for "
576                         + node.getType());
577 
578             }
579         }
580 
581         private final Class<? extends Object> wrapIfPrimitive(Class<?> clazz) {
582             if (!clazz.isPrimitive()) {
583                 return clazz;
584             }
585             if (clazz == Integer.TYPE) {
586                 return Integer.class;
587             }
588             if (clazz == Float.TYPE) {
589                 return Float.class;
590             }
591             if (clazz == Double.TYPE) {
592                 return Double.class;
593             }
594             if (clazz == Boolean.TYPE) {
595                 return Boolean.class;
596             }
597             if (clazz == Long.TYPE) {
598                 return Long.class;
599             }
600             if (clazz == Character.TYPE) {
601                 return Character.class;
602             }
603             if (clazz == Short.TYPE) {
604                 return Short.class;
605             }
606             if (clazz == Byte.TYPE) {
607                 return Byte.class;
608             }
609             throw new YAMLException("Unexpected primitive " + clazz);
610         }
611 
612         @SuppressWarnings("unchecked")
613         public void construct2ndStep(Node node, Object object) {
614             SequenceNode snode = (SequenceNode) node;
615             if (List.class.isAssignableFrom(node.getType())) {
616                 List<Object> list = (List<Object>) object;
617                 constructSequenceStep2(snode, list);
618             } else if (node.getType().isArray()) {
619                 constructArrayStep2(snode, object);
620             } else {
621                 throw new YAMLException("Immutable objects cannot be recursive.");
622             }
623         }
624     }
625 
626     protected Class<?> getClassForNode(Node node) {
627         Class<? extends Object> classForTag = typeTags.get(node.getTag());
628         if (classForTag == null) {
629             String name = node.getTag().getClassName();
630             Class<?> cl;
631             try {
632                 cl = getClassForName(name);
633             } catch (ClassNotFoundException e) {
634                 throw new YAMLException("Class not found: " + name);
635             }
636             typeTags.put(node.getTag(), cl);
637             return cl;
638         } else {
639             return classForTag;
640         }
641     }
642 
643     protected Class<?> getClassForName(String name) throws ClassNotFoundException {
644         return Class.forName(name);
645     }
646 }