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