• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (c) 2008, SnakeYAML
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package org.yaml.snakeyaml.constructor;
15 
16 import java.math.BigInteger;
17 import java.util.ArrayList;
18 import java.util.Calendar;
19 import java.util.HashMap;
20 import java.util.Iterator;
21 import java.util.LinkedHashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.TimeZone;
26 import java.util.TreeSet;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 import org.yaml.snakeyaml.LoaderOptions;
30 import org.yaml.snakeyaml.error.YAMLException;
31 import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
32 import org.yaml.snakeyaml.nodes.MappingNode;
33 import org.yaml.snakeyaml.nodes.Node;
34 import org.yaml.snakeyaml.nodes.NodeId;
35 import org.yaml.snakeyaml.nodes.NodeTuple;
36 import org.yaml.snakeyaml.nodes.ScalarNode;
37 import org.yaml.snakeyaml.nodes.SequenceNode;
38 import org.yaml.snakeyaml.nodes.Tag;
39 
40 /**
41  * Construct standard Java classes
42  */
43 public class SafeConstructor extends BaseConstructor {
44 
45   public static final ConstructUndefined undefinedConstructor = new ConstructUndefined();
46 
SafeConstructor()47   public SafeConstructor() {
48     this(new LoaderOptions());
49   }
50 
SafeConstructor(LoaderOptions loadingConfig)51   public SafeConstructor(LoaderOptions loadingConfig) {
52     super(loadingConfig);
53     this.yamlConstructors.put(Tag.NULL, new ConstructYamlNull());
54     this.yamlConstructors.put(Tag.BOOL, new ConstructYamlBool());
55     this.yamlConstructors.put(Tag.INT, new ConstructYamlInt());
56     this.yamlConstructors.put(Tag.FLOAT, new ConstructYamlFloat());
57     this.yamlConstructors.put(Tag.BINARY, new ConstructYamlBinary());
58     this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlTimestamp());
59     this.yamlConstructors.put(Tag.OMAP, new ConstructYamlOmap());
60     this.yamlConstructors.put(Tag.PAIRS, new ConstructYamlPairs());
61     this.yamlConstructors.put(Tag.SET, new ConstructYamlSet());
62     this.yamlConstructors.put(Tag.STR, new ConstructYamlStr());
63     this.yamlConstructors.put(Tag.SEQ, new ConstructYamlSeq());
64     this.yamlConstructors.put(Tag.MAP, new ConstructYamlMap());
65     this.yamlConstructors.put(null, undefinedConstructor);
66     this.yamlClassConstructors.put(NodeId.scalar, undefinedConstructor);
67     this.yamlClassConstructors.put(NodeId.sequence, undefinedConstructor);
68     this.yamlClassConstructors.put(NodeId.mapping, undefinedConstructor);
69   }
70 
flattenMapping(MappingNode node)71   protected void flattenMapping(MappingNode node) {
72     flattenMapping(node, false);
73   }
74 
flattenMapping(MappingNode node, boolean forceStringKeys)75   protected void flattenMapping(MappingNode node, boolean forceStringKeys) {
76     // perform merging only on nodes containing merge node(s)
77     processDuplicateKeys(node, forceStringKeys);
78     if (node.isMerged()) {
79       node.setValue(mergeNode(node, true, new HashMap<Object, Integer>(),
80           new ArrayList<NodeTuple>(), forceStringKeys));
81     }
82   }
83 
processDuplicateKeys(MappingNode node)84   protected void processDuplicateKeys(MappingNode node) {
85     processDuplicateKeys(node, false);
86   }
87 
processDuplicateKeys(MappingNode node, boolean forceStringKeys)88   protected void processDuplicateKeys(MappingNode node, boolean forceStringKeys) {
89     List<NodeTuple> nodeValue = node.getValue();
90     Map<Object, Integer> keys = new HashMap<Object, Integer>(nodeValue.size());
91     TreeSet<Integer> toRemove = new TreeSet<Integer>();
92     int i = 0;
93     for (NodeTuple tuple : nodeValue) {
94       Node keyNode = tuple.getKeyNode();
95       if (!keyNode.getTag().equals(Tag.MERGE)) {
96         if (forceStringKeys) {
97           if (keyNode instanceof ScalarNode) {
98             keyNode.setType(String.class);
99             keyNode.setTag(Tag.STR);
100           } else {
101             throw new YAMLException("Keys must be scalars but found: " + keyNode);
102           }
103         }
104         Object key = constructObject(keyNode);
105         if (key != null && !forceStringKeys) {
106           if (keyNode.isTwoStepsConstruction()) {
107             if (!loadingConfig.getAllowRecursiveKeys()) {
108               throw new YAMLException(
109                   "Recursive key for mapping is detected but it is not configured to be allowed.");
110             } else {
111               try {
112                 key.hashCode();// check circular dependencies
113               } catch (Exception e) {
114                 throw new ConstructorException("while constructing a mapping", node.getStartMark(),
115                     "found unacceptable key " + key, tuple.getKeyNode().getStartMark(), e);
116               }
117             }
118           }
119         }
120 
121         Integer prevIndex = keys.put(key, i);
122         if (prevIndex != null) {
123           if (!isAllowDuplicateKeys()) {
124             throw new DuplicateKeyException(node.getStartMark(), key,
125                 tuple.getKeyNode().getStartMark());
126           }
127           toRemove.add(prevIndex);
128         }
129       }
130       i = i + 1;
131     }
132 
133     Iterator<Integer> indices2remove = toRemove.descendingIterator();
134     while (indices2remove.hasNext()) {
135       nodeValue.remove(indices2remove.next().intValue());
136     }
137   }
138 
139   /**
140    * Does merge for supplied mapping node.
141    *
142    * @param node where to merge
143    * @param isPreffered true if keys of node should take precedence over others...
144    * @param key2index maps already merged keys to index from values
145    * @param values collects merged NodeTuple
146    * @return list of the merged NodeTuple (to be set as value for the MappingNode)
147    */
mergeNode(MappingNode node, boolean isPreffered, Map<Object, Integer> key2index, List<NodeTuple> values, boolean forceStringKeys)148   private List<NodeTuple> mergeNode(MappingNode node, boolean isPreffered,
149       Map<Object, Integer> key2index, List<NodeTuple> values, boolean forceStringKeys) {
150     Iterator<NodeTuple> iter = node.getValue().iterator();
151     while (iter.hasNext()) {
152       final NodeTuple nodeTuple = iter.next();
153       final Node keyNode = nodeTuple.getKeyNode();
154       final Node valueNode = nodeTuple.getValueNode();
155       if (keyNode.getTag().equals(Tag.MERGE)) {
156         iter.remove();
157         switch (valueNode.getNodeId()) {
158           case mapping:
159             MappingNode mn = (MappingNode) valueNode;
160             mergeNode(mn, false, key2index, values, forceStringKeys);
161             break;
162           case sequence:
163             SequenceNode sn = (SequenceNode) valueNode;
164             List<Node> vals = sn.getValue();
165             for (Node subnode : vals) {
166               if (!(subnode instanceof MappingNode)) {
167                 throw new ConstructorException("while constructing a mapping", node.getStartMark(),
168                     "expected a mapping for merging, but found " + subnode.getNodeId(),
169                     subnode.getStartMark());
170               }
171               MappingNode mnode = (MappingNode) subnode;
172               mergeNode(mnode, false, key2index, values, forceStringKeys);
173             }
174             break;
175           default:
176             throw new ConstructorException("while constructing a mapping", node.getStartMark(),
177                 "expected a mapping or list of mappings for merging, but found "
178                     + valueNode.getNodeId(),
179                 valueNode.getStartMark());
180         }
181       } else {
182         // we need to construct keys to avoid duplications
183         if (forceStringKeys) {
184           if (keyNode instanceof ScalarNode) {
185             keyNode.setType(String.class);
186             keyNode.setTag(Tag.STR);
187           } else {
188             throw new YAMLException("Keys must be scalars but found: " + keyNode);
189           }
190         }
191         Object key = constructObject(keyNode);
192         if (!key2index.containsKey(key)) { // 1st time merging key
193           values.add(nodeTuple);
194           // keep track where tuple for the key is
195           key2index.put(key, values.size() - 1);
196         } else if (isPreffered) { // there is value for the key, but we
197           // need to override it
198           // change value for the key using saved position
199           values.set(key2index.get(key), nodeTuple);
200         }
201       }
202     }
203     return values;
204   }
205 
206   @Override
constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping)207   protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) {
208     flattenMapping(node);
209     super.constructMapping2ndStep(node, mapping);
210   }
211 
212   @Override
constructSet2ndStep(MappingNode node, Set<Object> set)213   protected void constructSet2ndStep(MappingNode node, Set<Object> set) {
214     flattenMapping(node);
215     super.constructSet2ndStep(node, set);
216   }
217 
218   public class ConstructYamlNull extends AbstractConstruct {
219 
220     @Override
construct(Node node)221     public Object construct(Node node) {
222       if (node != null) {
223         constructScalar((ScalarNode) node);
224       }
225       return null;
226     }
227   }
228 
229   private final static Map<String, Boolean> BOOL_VALUES = new HashMap<String, Boolean>();
230 
231   static {
232     BOOL_VALUES.put("yes", Boolean.TRUE);
233     BOOL_VALUES.put("no", Boolean.FALSE);
234     BOOL_VALUES.put("true", Boolean.TRUE);
235     BOOL_VALUES.put("false", Boolean.FALSE);
236     BOOL_VALUES.put("on", Boolean.TRUE);
237     BOOL_VALUES.put("off", Boolean.FALSE);
238   }
239 
240   public class ConstructYamlBool extends AbstractConstruct {
241 
242     @Override
construct(Node node)243     public Object construct(Node node) {
244       String val = constructScalar((ScalarNode) node);
245       return BOOL_VALUES.get(val.toLowerCase());
246     }
247   }
248 
249   public class ConstructYamlInt extends AbstractConstruct {
250 
251     @Override
construct(Node node)252     public Object construct(Node node) {
253       String value = constructScalar((ScalarNode) node).replaceAll("_", "");
254       if (value.isEmpty()) {
255         throw new ConstructorException("while constructing an int", node.getStartMark(),
256             "found empty value", node.getStartMark());
257       }
258       int sign = +1;
259       char first = value.charAt(0);
260       if (first == '-') {
261         sign = -1;
262         value = value.substring(1);
263       } else if (first == '+') {
264         value = value.substring(1);
265       }
266       int base = 10;
267       if ("0".equals(value)) {
268         return Integer.valueOf(0);
269       } else if (value.startsWith("0b")) {
270         value = value.substring(2);
271         base = 2;
272       } else if (value.startsWith("0x")) {
273         value = value.substring(2);
274         base = 16;
275       } else if (value.startsWith("0")) {
276         value = value.substring(1);
277         base = 8;
278       } else if (value.indexOf(':') != -1) {
279         String[] digits = value.split(":");
280         int bes = 1;
281         int val = 0;
282         for (int i = 0, j = digits.length; i < j; i++) {
283           val += Long.parseLong(digits[j - i - 1]) * bes;
284           bes *= 60;
285         }
286         return createNumber(sign, String.valueOf(val), 10);
287       } else {
288         return createNumber(sign, value, 10);
289       }
290       return createNumber(sign, value, base);
291     }
292   }
293 
294   private static final int[][] RADIX_MAX = new int[17][2];
295 
296   static {
297     int[] radixList = new int[] {2, 8, 10, 16};
298     for (int radix : radixList) {
299       RADIX_MAX[radix] =
300           new int[] {maxLen(Integer.MAX_VALUE, radix), maxLen(Long.MAX_VALUE, radix)};
301     }
302   }
303 
maxLen(final int max, final int radix)304   private static int maxLen(final int max, final int radix) {
305     return Integer.toString(max, radix).length();
306   }
307 
maxLen(final long max, final int radix)308   private static int maxLen(final long max, final int radix) {
309     return Long.toString(max, radix).length();
310   }
311 
createNumber(int sign, String number, int radix)312   private Number createNumber(int sign, String number, int radix) {
313     final int len = number != null ? number.length() : 0;
314     if (sign < 0) {
315       number = "-" + number;
316     }
317     final int[] maxArr = radix < RADIX_MAX.length ? RADIX_MAX[radix] : null;
318     if (maxArr != null) {
319       final boolean gtInt = len > maxArr[0];
320       if (gtInt) {
321         if (len > maxArr[1]) {
322           return new BigInteger(number, radix);
323         }
324         return createLongOrBigInteger(number, radix);
325       }
326     }
327     Number result;
328     try {
329       result = Integer.valueOf(number, radix);
330     } catch (NumberFormatException e) {
331       result = createLongOrBigInteger(number, radix);
332     }
333     return result;
334   }
335 
createLongOrBigInteger(final String number, final int radix)336   protected static Number createLongOrBigInteger(final String number, final int radix) {
337     try {
338       return Long.valueOf(number, radix);
339     } catch (NumberFormatException e1) {
340       return new BigInteger(number, radix);
341     }
342   }
343 
344   public class ConstructYamlFloat extends AbstractConstruct {
345 
346     @Override
construct(Node node)347     public Object construct(Node node) {
348       String value = constructScalar((ScalarNode) node).replaceAll("_", "");
349       if (value.isEmpty()) {
350         throw new ConstructorException("while constructing a float", node.getStartMark(),
351             "found empty value", node.getStartMark());
352       }
353       int sign = +1;
354       char first = value.charAt(0);
355       if (first == '-') {
356         sign = -1;
357         value = value.substring(1);
358       } else if (first == '+') {
359         value = value.substring(1);
360       }
361       String valLower = value.toLowerCase();
362       if (".inf".equals(valLower)) {
363         return Double.valueOf(sign == -1 ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY);
364       } else if (".nan".equals(valLower)) {
365         return Double.valueOf(Double.NaN);
366       } else if (value.indexOf(':') != -1) {
367         String[] digits = value.split(":");
368         int bes = 1;
369         double val = 0.0;
370         for (int i = 0, j = digits.length; i < j; i++) {
371           val += Double.parseDouble(digits[j - i - 1]) * bes;
372           bes *= 60;
373         }
374         return Double.valueOf(sign * val);
375       } else {
376         Double d = Double.valueOf(value);
377         return Double.valueOf(d.doubleValue() * sign);
378       }
379     }
380   }
381 
382   public class ConstructYamlBinary extends AbstractConstruct {
383 
384     @Override
construct(Node node)385     public Object construct(Node node) {
386       // Ignore white spaces for base64 encoded scalar
387       String noWhiteSpaces = constructScalar((ScalarNode) node).replaceAll("\\s", "");
388       byte[] decoded = Base64Coder.decode(noWhiteSpaces.toCharArray());
389       return decoded;
390     }
391   }
392 
393   private final static Pattern TIMESTAMP_REGEXP = Pattern.compile(
394       "^([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])?)?))?)?$");
395   private final static Pattern YMD_REGEXP =
396       Pattern.compile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$");
397 
398   public static class ConstructYamlTimestamp extends AbstractConstruct {
399 
400     private Calendar calendar;
401 
getCalendar()402     public Calendar getCalendar() {
403       return calendar;
404     }
405 
406     @Override
construct(Node node)407     public Object construct(Node node) {
408       ScalarNode scalar = (ScalarNode) node;
409       String nodeValue = scalar.getValue();
410       Matcher match = YMD_REGEXP.matcher(nodeValue);
411       if (match.matches()) {
412         String year_s = match.group(1);
413         String month_s = match.group(2);
414         String day_s = match.group(3);
415         calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
416         calendar.clear();
417         calendar.set(Calendar.YEAR, Integer.parseInt(year_s));
418         // Java's months are zero-based...
419         calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1); // x
420         calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s));
421         return calendar.getTime();
422       } else {
423         match = TIMESTAMP_REGEXP.matcher(nodeValue);
424         if (!match.matches()) {
425           throw new YAMLException("Unexpected timestamp: " + nodeValue);
426         }
427         String year_s = match.group(1);
428         String month_s = match.group(2);
429         String day_s = match.group(3);
430         String hour_s = match.group(4);
431         String min_s = match.group(5);
432         // seconds and milliseconds
433         String seconds = match.group(6);
434         String millis = match.group(7);
435         if (millis != null) {
436           seconds = seconds + "." + millis;
437         }
438         double fractions = Double.parseDouble(seconds);
439         int sec_s = (int) Math.round(Math.floor(fractions));
440         int usec = (int) Math.round((fractions - sec_s) * 1000);
441         // timezone
442         String timezoneh_s = match.group(8);
443         String timezonem_s = match.group(9);
444         TimeZone timeZone;
445         if (timezoneh_s != null) {
446           String time = timezonem_s != null ? ":" + timezonem_s : "00";
447           timeZone = TimeZone.getTimeZone("GMT" + timezoneh_s + time);
448         } else {
449           // no time zone provided
450           timeZone = TimeZone.getTimeZone("UTC");
451         }
452         calendar = Calendar.getInstance(timeZone);
453         calendar.set(Calendar.YEAR, Integer.parseInt(year_s));
454         // Java's months are zero-based...
455         calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1);
456         calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s));
457         calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour_s));
458         calendar.set(Calendar.MINUTE, Integer.parseInt(min_s));
459         calendar.set(Calendar.SECOND, sec_s);
460         calendar.set(Calendar.MILLISECOND, usec);
461         return calendar.getTime();
462       }
463     }
464   }
465 
466   public class ConstructYamlOmap extends AbstractConstruct {
467 
468     @Override
construct(Node node)469     public Object construct(Node node) {
470       // Note: we do not check for duplicate keys, because it's too
471       // CPU-expensive.
472       Map<Object, Object> omap = new LinkedHashMap<Object, Object>();
473       if (!(node instanceof SequenceNode)) {
474         throw new ConstructorException("while constructing an ordered map", node.getStartMark(),
475             "expected a sequence, but found " + node.getNodeId(), node.getStartMark());
476       }
477       SequenceNode snode = (SequenceNode) node;
478       for (Node subnode : snode.getValue()) {
479         if (!(subnode instanceof MappingNode)) {
480           throw new ConstructorException("while constructing an ordered map", node.getStartMark(),
481               "expected a mapping of length 1, but found " + subnode.getNodeId(),
482               subnode.getStartMark());
483         }
484         MappingNode mnode = (MappingNode) subnode;
485         if (mnode.getValue().size() != 1) {
486           throw new ConstructorException("while constructing an ordered map", node.getStartMark(),
487               "expected a single mapping item, but found " + mnode.getValue().size() + " items",
488               mnode.getStartMark());
489         }
490         Node keyNode = mnode.getValue().get(0).getKeyNode();
491         Node valueNode = mnode.getValue().get(0).getValueNode();
492         Object key = constructObject(keyNode);
493         Object value = constructObject(valueNode);
494         omap.put(key, value);
495       }
496       return omap;
497     }
498   }
499 
500   public class ConstructYamlPairs extends AbstractConstruct {
501 
502     @Override
construct(Node node)503     public Object construct(Node node) {
504       // Note: we do not check for duplicate keys, because it's too
505       // CPU-expensive.
506       if (!(node instanceof SequenceNode)) {
507         throw new ConstructorException("while constructing pairs", node.getStartMark(),
508             "expected a sequence, but found " + node.getNodeId(), node.getStartMark());
509       }
510       SequenceNode snode = (SequenceNode) node;
511       List<Object[]> pairs = new ArrayList<Object[]>(snode.getValue().size());
512       for (Node subnode : snode.getValue()) {
513         if (!(subnode instanceof MappingNode)) {
514           throw new ConstructorException("while constructingpairs", node.getStartMark(),
515               "expected a mapping of length 1, but found " + subnode.getNodeId(),
516               subnode.getStartMark());
517         }
518         MappingNode mnode = (MappingNode) subnode;
519         if (mnode.getValue().size() != 1) {
520           throw new ConstructorException("while constructing pairs", node.getStartMark(),
521               "expected a single mapping item, but found " + mnode.getValue().size() + " items",
522               mnode.getStartMark());
523         }
524         Node keyNode = mnode.getValue().get(0).getKeyNode();
525         Node valueNode = mnode.getValue().get(0).getValueNode();
526         Object key = constructObject(keyNode);
527         Object value = constructObject(valueNode);
528         pairs.add(new Object[] {key, value});
529       }
530       return pairs;
531     }
532   }
533 
534   public class ConstructYamlSet implements Construct {
535 
536     @Override
construct(Node node)537     public Object construct(Node node) {
538       if (node.isTwoStepsConstruction()) {
539         return (constructedObjects.containsKey(node) ? constructedObjects.get(node)
540             : createDefaultSet(((MappingNode) node).getValue().size()));
541       } else {
542         return constructSet((MappingNode) node);
543       }
544     }
545 
546     @Override
547     @SuppressWarnings("unchecked")
construct2ndStep(Node node, Object object)548     public void construct2ndStep(Node node, Object object) {
549       if (node.isTwoStepsConstruction()) {
550         constructSet2ndStep((MappingNode) node, (Set<Object>) object);
551       } else {
552         throw new YAMLException("Unexpected recursive set structure. Node: " + node);
553       }
554     }
555   }
556 
557   public class ConstructYamlStr extends AbstractConstruct {
558 
559     @Override
construct(Node node)560     public Object construct(Node node) {
561       return constructScalar((ScalarNode) node);
562     }
563   }
564 
565   public class ConstructYamlSeq implements Construct {
566 
567     @Override
construct(Node node)568     public Object construct(Node node) {
569       SequenceNode seqNode = (SequenceNode) node;
570       if (node.isTwoStepsConstruction()) {
571         return newList(seqNode);
572       } else {
573         return constructSequence(seqNode);
574       }
575     }
576 
577     @Override
578     @SuppressWarnings("unchecked")
construct2ndStep(Node node, Object data)579     public void construct2ndStep(Node node, Object data) {
580       if (node.isTwoStepsConstruction()) {
581         constructSequenceStep2((SequenceNode) node, (List<Object>) data);
582       } else {
583         throw new YAMLException("Unexpected recursive sequence structure. Node: " + node);
584       }
585     }
586   }
587 
588   public class ConstructYamlMap implements Construct {
589 
590     @Override
construct(Node node)591     public Object construct(Node node) {
592       MappingNode mnode = (MappingNode) node;
593       if (node.isTwoStepsConstruction()) {
594         return createDefaultMap(mnode.getValue().size());
595       } else {
596         return constructMapping(mnode);
597       }
598     }
599 
600     @Override
601     @SuppressWarnings("unchecked")
construct2ndStep(Node node, Object object)602     public void construct2ndStep(Node node, Object object) {
603       if (node.isTwoStepsConstruction()) {
604         constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object);
605       } else {
606         throw new YAMLException("Unexpected recursive mapping structure. Node: " + node);
607       }
608     }
609   }
610 
611   public static final class ConstructUndefined extends AbstractConstruct {
612 
613     @Override
construct(Node node)614     public Object construct(Node node) {
615       throw new ConstructorException(null, null,
616           "could not determine a constructor for the tag " + node.getTag(), node.getStartMark());
617     }
618   }
619 }
620