Home | History | Annotate | Download | only in constructor
      1 /**
      2  * Copyright (c) 2008, 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.math.BigInteger;
     19 import java.text.NumberFormat;
     20 import java.text.ParseException;
     21 import java.util.ArrayList;
     22 import java.util.Calendar;
     23 import java.util.Collections;
     24 import java.util.HashMap;
     25 import java.util.Iterator;
     26 import java.util.LinkedHashMap;
     27 import java.util.List;
     28 import java.util.Map;
     29 import java.util.Set;
     30 import java.util.TimeZone;
     31 import java.util.regex.Matcher;
     32 import java.util.regex.Pattern;
     33 
     34 import org.yaml.snakeyaml.error.YAMLException;
     35 import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
     36 import org.yaml.snakeyaml.nodes.MappingNode;
     37 import org.yaml.snakeyaml.nodes.Node;
     38 import org.yaml.snakeyaml.nodes.NodeId;
     39 import org.yaml.snakeyaml.nodes.NodeTuple;
     40 import org.yaml.snakeyaml.nodes.ScalarNode;
     41 import org.yaml.snakeyaml.nodes.SequenceNode;
     42 import org.yaml.snakeyaml.nodes.Tag;
     43 
     44 /**
     45  * Construct standard Java classes
     46  */
     47 public class SafeConstructor extends BaseConstructor {
     48 
     49     public static final ConstructUndefined undefinedConstructor = new ConstructUndefined();
     50 
     51     public SafeConstructor() {
     52         this.yamlConstructors.put(Tag.NULL, new ConstructYamlNull());
     53         this.yamlConstructors.put(Tag.BOOL, new ConstructYamlBool());
     54         this.yamlConstructors.put(Tag.INT, new ConstructYamlInt());
     55         this.yamlConstructors.put(Tag.FLOAT, new ConstructYamlFloat());
     56         this.yamlConstructors.put(Tag.BINARY, new ConstructYamlBinary());
     57         this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlTimestamp());
     58         this.yamlConstructors.put(Tag.OMAP, new ConstructYamlOmap());
     59         this.yamlConstructors.put(Tag.PAIRS, new ConstructYamlPairs());
     60         this.yamlConstructors.put(Tag.SET, new ConstructYamlSet());
     61         this.yamlConstructors.put(Tag.STR, new ConstructYamlStr());
     62         this.yamlConstructors.put(Tag.SEQ, new ConstructYamlSeq());
     63         this.yamlConstructors.put(Tag.MAP, new ConstructYamlMap());
     64         this.yamlConstructors.put(null, undefinedConstructor);
     65         this.yamlClassConstructors.put(NodeId.scalar, undefinedConstructor);
     66         this.yamlClassConstructors.put(NodeId.sequence, undefinedConstructor);
     67         this.yamlClassConstructors.put(NodeId.mapping, undefinedConstructor);
     68     }
     69 
     70     protected void flattenMapping(MappingNode node) {
     71         // perform merging only on nodes containing merge node(s)
     72         if (node.isMerged()) {
     73             node.setValue(mergeNode(node, true, new HashMap<Object, Integer>(),
     74                     new ArrayList<NodeTuple>()));
     75         }
     76     }
     77 
     78     /**
     79      * Does merge for supplied mapping node.
     80      *
     81      * @param node
     82      *            where to merge
     83      * @param isPreffered
     84      *            true if keys of node should take precedence over others...
     85      * @param key2index
     86      *            maps already merged keys to index from values
     87      * @param values
     88      *            collects merged NodeTuple
     89      * @return list of the merged NodeTuple (to be set as value for the
     90      *         MappingNode)
     91      */
     92     private List<NodeTuple> mergeNode(MappingNode node, boolean isPreffered,
     93             Map<Object, Integer> key2index, List<NodeTuple> values) {
     94         List<NodeTuple> nodeValue = node.getValue();
     95         // reversed for http://code.google.com/p/snakeyaml/issues/detail?id=139
     96         Collections.reverse(nodeValue);
     97         for (Iterator<NodeTuple> iter = nodeValue.iterator(); iter.hasNext();) {
     98             final NodeTuple nodeTuple = iter.next();
     99             final Node keyNode = nodeTuple.getKeyNode();
    100             final Node valueNode = nodeTuple.getValueNode();
    101             if (keyNode.getTag().equals(Tag.MERGE)) {
    102                 iter.remove();
    103                 switch (valueNode.getNodeId()) {
    104                 case mapping:
    105                     MappingNode mn = (MappingNode) valueNode;
    106                     mergeNode(mn, false, key2index, values);
    107                     break;
    108                 case sequence:
    109                     SequenceNode sn = (SequenceNode) valueNode;
    110                     List<Node> vals = sn.getValue();
    111                     for (Node subnode : vals) {
    112                         if (!(subnode instanceof MappingNode)) {
    113                             throw new ConstructorException("while constructing a mapping",
    114                                     node.getStartMark(),
    115                                     "expected a mapping for merging, but found "
    116                                             + subnode.getNodeId(), subnode.getStartMark());
    117                         }
    118                         MappingNode mnode = (MappingNode) subnode;
    119                         mergeNode(mnode, false, key2index, values);
    120                     }
    121                     break;
    122                 default:
    123                     throw new ConstructorException("while constructing a mapping",
    124                             node.getStartMark(),
    125                             "expected a mapping or list of mappings for merging, but found "
    126                                     + valueNode.getNodeId(), valueNode.getStartMark());
    127                 }
    128             } else {
    129                 // we need to construct keys to avoid duplications
    130                 Object key = constructObject(keyNode);
    131                 if (!key2index.containsKey(key)) { // 1st time merging key
    132                     values.add(nodeTuple);
    133                     // keep track where tuple for the key is
    134                     key2index.put(key, values.size() - 1);
    135                 } else if (isPreffered) { // there is value for the key, but we
    136                                           // need to override it
    137                     // change value for the key using saved position
    138                     values.set(key2index.get(key), nodeTuple);
    139                 }
    140             }
    141         }
    142         return values;
    143     }
    144 
    145     protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) {
    146         flattenMapping(node);
    147         super.constructMapping2ndStep(node, mapping);
    148     }
    149 
    150     @Override
    151     protected void constructSet2ndStep(MappingNode node, Set<Object> set) {
    152         flattenMapping(node);
    153         super.constructSet2ndStep(node, set);
    154     }
    155 
    156     public class ConstructYamlNull extends AbstractConstruct {
    157         public Object construct(Node node) {
    158             constructScalar((ScalarNode) node);
    159             return null;
    160         }
    161     }
    162 
    163     private final static Map<String, Boolean> BOOL_VALUES = new HashMap<String, Boolean>();
    164     static {
    165         BOOL_VALUES.put("yes", Boolean.TRUE);
    166         BOOL_VALUES.put("no", Boolean.FALSE);
    167         BOOL_VALUES.put("true", Boolean.TRUE);
    168         BOOL_VALUES.put("false", Boolean.FALSE);
    169         BOOL_VALUES.put("on", Boolean.TRUE);
    170         BOOL_VALUES.put("off", Boolean.FALSE);
    171     }
    172 
    173     public class ConstructYamlBool extends AbstractConstruct {
    174         public Object construct(Node node) {
    175             String val = (String) constructScalar((ScalarNode) node);
    176             return BOOL_VALUES.get(val.toLowerCase());
    177         }
    178     }
    179 
    180     public class ConstructYamlInt extends AbstractConstruct {
    181         public Object construct(Node node) {
    182             String value = constructScalar((ScalarNode) node).toString().replaceAll("_", "");
    183             int sign = +1;
    184             char first = value.charAt(0);
    185             if (first == '-') {
    186                 sign = -1;
    187                 value = value.substring(1);
    188             } else if (first == '+') {
    189                 value = value.substring(1);
    190             }
    191             int base = 10;
    192             if ("0".equals(value)) {
    193                 return Integer.valueOf(0);
    194             } else if (value.startsWith("0b")) {
    195                 value = value.substring(2);
    196                 base = 2;
    197             } else if (value.startsWith("0x")) {
    198                 value = value.substring(2);
    199                 base = 16;
    200             } else if (value.startsWith("0")) {
    201                 value = value.substring(1);
    202                 base = 8;
    203             } else if (value.indexOf(':') != -1) {
    204                 String[] digits = value.split(":");
    205                 int bes = 1;
    206                 int val = 0;
    207                 for (int i = 0, j = digits.length; i < j; i++) {
    208                     val += Long.parseLong(digits[j - i - 1]) * bes;
    209                     bes *= 60;
    210                 }
    211                 return createNumber(sign, String.valueOf(val), 10);
    212             } else {
    213                 return createNumber(sign, value, 10);
    214             }
    215             return createNumber(sign, value, base);
    216         }
    217     }
    218 
    219     private Number createNumber(int sign, String number, int radix) {
    220         Number result;
    221         if (sign < 0) {
    222             number = "-" + number;
    223         }
    224         try {
    225             result = Integer.valueOf(number, radix);
    226         } catch (NumberFormatException e) {
    227             try {
    228                 result = Long.valueOf(number, radix);
    229             } catch (NumberFormatException e1) {
    230                 result = new BigInteger(number, radix);
    231             }
    232         }
    233         return result;
    234     }
    235 
    236     public class ConstructYamlFloat extends AbstractConstruct {
    237         public Object construct(Node node) {
    238             String value = constructScalar((ScalarNode) node).toString().replaceAll("_", "");
    239             int sign = +1;
    240             char first = value.charAt(0);
    241             if (first == '-') {
    242                 sign = -1;
    243                 value = value.substring(1);
    244             } else if (first == '+') {
    245                 value = value.substring(1);
    246             }
    247             String valLower = value.toLowerCase();
    248             if (".inf".equals(valLower)) {
    249                 return new Double(sign == -1 ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY);
    250             } else if (".nan".equals(valLower)) {
    251                 return new Double(Double.NaN);
    252             } else if (value.indexOf(':') != -1) {
    253                 String[] digits = value.split(":");
    254                 int bes = 1;
    255                 double val = 0.0;
    256                 for (int i = 0, j = digits.length; i < j; i++) {
    257                     val += Double.parseDouble(digits[j - i - 1]) * bes;
    258                     bes *= 60;
    259                 }
    260                 return new Double(sign * val);
    261             } else {
    262                 Double d = Double.valueOf(value);
    263                 return new Double(d.doubleValue() * sign);
    264             }
    265         }
    266     }
    267 
    268     public class ConstructYamlBinary extends AbstractConstruct {
    269         public Object construct(Node node) {
    270             byte[] decoded = Base64Coder.decode(constructScalar((ScalarNode) node).toString()
    271                     .toCharArray());
    272             return decoded;
    273         }
    274     }
    275 
    276     public class ConstructYamlNumber extends AbstractConstruct {
    277 
    278         private final NumberFormat nf = NumberFormat.getInstance();
    279 
    280         public Object construct(Node node) {
    281             ScalarNode scalar = (ScalarNode) node;
    282             try {
    283                 return nf.parse(scalar.getValue());
    284             } catch (ParseException e) {
    285                 String lowerCaseValue = scalar.getValue().toLowerCase();
    286                 if (lowerCaseValue.contains("inf") || lowerCaseValue.contains("nan")) {
    287                     /*
    288                      * Non-finites such as (+/-)infinity and NaN are not
    289                      * parseable by NumberFormat when these `Double` values are
    290                      * dumped by snakeyaml. Delegate to the `Tag.FLOAT`
    291                      * constructor when for this expected failure cause.
    292                      */
    293                     return (Number) yamlConstructors.get(Tag.FLOAT).construct(node);
    294                 } else {
    295                     throw new IllegalArgumentException("Unable to parse as Number: "
    296                             + scalar.getValue());
    297                 }
    298             }
    299         }
    300     }
    301 
    302     private final static Pattern TIMESTAMP_REGEXP = Pattern
    303             .compile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:(?:[Tt]|[ \t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \t]*(?:Z|([-+][0-9][0-9]?)(?::([0-9][0-9])?)?))?)?$");
    304     private final static Pattern YMD_REGEXP = Pattern
    305             .compile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$");
    306 
    307     public static class ConstructYamlTimestamp extends AbstractConstruct {
    308         private Calendar calendar;
    309 
    310         public Calendar getCalendar() {
    311             return calendar;
    312         }
    313 
    314         public Object construct(Node node) {
    315             ScalarNode scalar = (ScalarNode) node;
    316             String nodeValue = scalar.getValue();
    317             Matcher match = YMD_REGEXP.matcher(nodeValue);
    318             if (match.matches()) {
    319                 String year_s = match.group(1);
    320                 String month_s = match.group(2);
    321                 String day_s = match.group(3);
    322                 calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
    323                 calendar.clear();
    324                 calendar.set(Calendar.YEAR, Integer.parseInt(year_s));
    325                 // Java's months are zero-based...
    326                 calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1); // x
    327                 calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s));
    328                 return calendar.getTime();
    329             } else {
    330                 match = TIMESTAMP_REGEXP.matcher(nodeValue);
    331                 if (!match.matches()) {
    332                     throw new YAMLException("Unexpected timestamp: " + nodeValue);
    333                 }
    334                 String year_s = match.group(1);
    335                 String month_s = match.group(2);
    336                 String day_s = match.group(3);
    337                 String hour_s = match.group(4);
    338                 String min_s = match.group(5);
    339                 // seconds and milliseconds
    340                 String seconds = match.group(6);
    341                 String millis = match.group(7);
    342                 if (millis != null) {
    343                     seconds = seconds + "." + millis;
    344                 }
    345                 double fractions = Double.parseDouble(seconds);
    346                 int sec_s = (int) Math.round(Math.floor(fractions));
    347                 int usec = (int) Math.round((fractions - sec_s) * 1000);
    348                 // timezone
    349                 String timezoneh_s = match.group(8);
    350                 String timezonem_s = match.group(9);
    351                 TimeZone timeZone;
    352                 if (timezoneh_s != null) {
    353                     String time = timezonem_s != null ? ":" + timezonem_s : "00";
    354                     timeZone = TimeZone.getTimeZone("GMT" + timezoneh_s + time);
    355                 } else {
    356                     // no time zone provided
    357                     timeZone = TimeZone.getTimeZone("UTC");
    358                 }
    359                 calendar = Calendar.getInstance(timeZone);
    360                 calendar.set(Calendar.YEAR, Integer.parseInt(year_s));
    361                 // Java's months are zero-based...
    362                 calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1);
    363                 calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s));
    364                 calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour_s));
    365                 calendar.set(Calendar.MINUTE, Integer.parseInt(min_s));
    366                 calendar.set(Calendar.SECOND, sec_s);
    367                 calendar.set(Calendar.MILLISECOND, usec);
    368                 return calendar.getTime();
    369             }
    370         }
    371     }
    372 
    373     public class ConstructYamlOmap extends AbstractConstruct {
    374         public Object construct(Node node) {
    375             // Note: we do not check for duplicate keys, because it's too
    376             // CPU-expensive.
    377             Map<Object, Object> omap = new LinkedHashMap<Object, Object>();
    378             if (!(node instanceof SequenceNode)) {
    379                 throw new ConstructorException("while constructing an ordered map",
    380                         node.getStartMark(), "expected a sequence, but found " + node.getNodeId(),
    381                         node.getStartMark());
    382             }
    383             SequenceNode snode = (SequenceNode) node;
    384             for (Node subnode : snode.getValue()) {
    385                 if (!(subnode instanceof MappingNode)) {
    386                     throw new ConstructorException("while constructing an ordered map",
    387                             node.getStartMark(), "expected a mapping of length 1, but found "
    388                                     + subnode.getNodeId(), subnode.getStartMark());
    389                 }
    390                 MappingNode mnode = (MappingNode) subnode;
    391                 if (mnode.getValue().size() != 1) {
    392                     throw new ConstructorException("while constructing an ordered map",
    393                             node.getStartMark(), "expected a single mapping item, but found "
    394                                     + mnode.getValue().size() + " items", mnode.getStartMark());
    395                 }
    396                 Node keyNode = mnode.getValue().get(0).getKeyNode();
    397                 Node valueNode = mnode.getValue().get(0).getValueNode();
    398                 Object key = constructObject(keyNode);
    399                 Object value = constructObject(valueNode);
    400                 omap.put(key, value);
    401             }
    402             return omap;
    403         }
    404     }
    405 
    406     // Note: the same code as `construct_yaml_omap`.
    407     public class ConstructYamlPairs extends AbstractConstruct {
    408         public Object construct(Node node) {
    409             // Note: we do not check for duplicate keys, because it's too
    410             // CPU-expensive.
    411             if (!(node instanceof SequenceNode)) {
    412                 throw new ConstructorException("while constructing pairs", node.getStartMark(),
    413                         "expected a sequence, but found " + node.getNodeId(), node.getStartMark());
    414             }
    415             SequenceNode snode = (SequenceNode) node;
    416             List<Object[]> pairs = new ArrayList<Object[]>(snode.getValue().size());
    417             for (Node subnode : snode.getValue()) {
    418                 if (!(subnode instanceof MappingNode)) {
    419                     throw new ConstructorException("while constructingpairs", node.getStartMark(),
    420                             "expected a mapping of length 1, but found " + subnode.getNodeId(),
    421                             subnode.getStartMark());
    422                 }
    423                 MappingNode mnode = (MappingNode) subnode;
    424                 if (mnode.getValue().size() != 1) {
    425                     throw new ConstructorException("while constructing pairs", node.getStartMark(),
    426                             "expected a single mapping item, but found " + mnode.getValue().size()
    427                                     + " items", mnode.getStartMark());
    428                 }
    429                 Node keyNode = mnode.getValue().get(0).getKeyNode();
    430                 Node valueNode = mnode.getValue().get(0).getValueNode();
    431                 Object key = constructObject(keyNode);
    432                 Object value = constructObject(valueNode);
    433                 pairs.add(new Object[] { key, value });
    434             }
    435             return pairs;
    436         }
    437     }
    438 
    439     public class ConstructYamlSet implements Construct {
    440         public Object construct(Node node) {
    441             if (node.isTwoStepsConstruction()) {
    442                 return createDefaultSet();
    443             } else {
    444                 return constructSet((MappingNode) node);
    445             }
    446         }
    447 
    448         @SuppressWarnings("unchecked")
    449         public void construct2ndStep(Node node, Object object) {
    450             if (node.isTwoStepsConstruction()) {
    451                 constructSet2ndStep((MappingNode) node, (Set<Object>) object);
    452             } else {
    453                 throw new YAMLException("Unexpected recursive set structure. Node: " + node);
    454             }
    455         }
    456     }
    457 
    458     public class ConstructYamlStr extends AbstractConstruct {
    459         public Object construct(Node node) {
    460             return constructScalar((ScalarNode) node);
    461         }
    462     }
    463 
    464     public class ConstructYamlSeq implements Construct {
    465         public Object construct(Node node) {
    466             SequenceNode seqNode = (SequenceNode) node;
    467             if (node.isTwoStepsConstruction()) {
    468                 return createDefaultList(seqNode.getValue().size());
    469             } else {
    470                 return constructSequence(seqNode);
    471             }
    472         }
    473 
    474         @SuppressWarnings("unchecked")
    475         public void construct2ndStep(Node node, Object data) {
    476             if (node.isTwoStepsConstruction()) {
    477                 constructSequenceStep2((SequenceNode) node, (List<Object>) data);
    478             } else {
    479                 throw new YAMLException("Unexpected recursive sequence structure. Node: " + node);
    480             }
    481         }
    482     }
    483 
    484     public class ConstructYamlMap implements Construct {
    485         public Object construct(Node node) {
    486             if (node.isTwoStepsConstruction()) {
    487                 return createDefaultMap();
    488             } else {
    489                 return constructMapping((MappingNode) node);
    490             }
    491         }
    492 
    493         @SuppressWarnings("unchecked")
    494         public void construct2ndStep(Node node, Object object) {
    495             if (node.isTwoStepsConstruction()) {
    496                 constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object);
    497             } else {
    498                 throw new YAMLException("Unexpected recursive mapping structure. Node: " + node);
    499             }
    500         }
    501     }
    502 
    503     public static final class ConstructUndefined extends AbstractConstruct {
    504         public Object construct(Node node) {
    505             throw new ConstructorException(null, null,
    506                     "could not determine a constructor for the tag " + node.getTag(),
    507                     node.getStartMark());
    508         }
    509     }
    510 }
    511