001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.databind.io.json; 007 008import com.fasterxml.jackson.core.JsonLocation; 009import com.fasterxml.jackson.core.JsonParser; 010import com.fasterxml.jackson.core.JsonToken; 011import com.fasterxml.jackson.databind.JsonNode; 012import com.fasterxml.jackson.databind.node.JsonNodeFactory; 013import com.fasterxml.jackson.databind.node.ObjectNode; 014 015import org.apache.logging.log4j.LogManager; 016import org.apache.logging.log4j.Logger; 017import org.eclipse.jdt.annotation.NotOwning; 018 019import java.io.IOException; 020import java.net.URI; 021import java.util.Collection; 022import java.util.Deque; 023import java.util.HashMap; 024import java.util.LinkedHashMap; 025import java.util.LinkedList; 026import java.util.List; 027import java.util.Map; 028 029import dev.metaschema.core.model.IAnyInstance; 030import dev.metaschema.core.model.IBoundObject; 031import dev.metaschema.core.model.IResourceLocation; 032import dev.metaschema.core.model.SimpleResourceLocation; 033import dev.metaschema.core.model.util.JsonUtil; 034import dev.metaschema.core.util.ObjectUtils; 035import dev.metaschema.databind.io.BindingException; 036import dev.metaschema.databind.io.Format; 037import dev.metaschema.databind.io.PathTracker; 038import dev.metaschema.databind.io.ValidationContext; 039import dev.metaschema.databind.model.IBoundDefinitionModelAssembly; 040import dev.metaschema.databind.model.IBoundDefinitionModelComplex; 041import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex; 042import dev.metaschema.databind.model.IBoundFieldValue; 043import dev.metaschema.databind.model.IBoundInstance; 044import dev.metaschema.databind.model.IBoundInstanceFlag; 045import dev.metaschema.databind.model.IBoundInstanceModel; 046import dev.metaschema.databind.model.IBoundInstanceModelAny; 047import dev.metaschema.databind.model.IBoundInstanceModelAssembly; 048import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup; 049import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex; 050import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar; 051import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly; 052import dev.metaschema.databind.model.IBoundInstanceModelGroupedField; 053import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed; 054import dev.metaschema.databind.model.IBoundProperty; 055import dev.metaschema.databind.model.info.AbstractModelInstanceReadHandler; 056import dev.metaschema.databind.model.info.IFeatureScalarItemValueHandler; 057import dev.metaschema.databind.model.info.IItemReadHandler; 058import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo; 059import edu.umd.cs.findbugs.annotations.NonNull; 060import edu.umd.cs.findbugs.annotations.Nullable; 061import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 062 063/** 064 * Supports reading JSON-based Metaschema module instances. 065 */ 066@SuppressWarnings({ 067 "PMD.CouplingBetweenObjects", 068 "PMD.GodClass" 069}) 070public class MetaschemaJsonReader 071 implements IJsonParsingContext, IItemReadHandler { 072 private static final Logger LOGGER = LogManager.getLogger(MetaschemaJsonReader.class); 073 074 @NonNull 075 private final Deque<JsonParser> parserStack = new LinkedList<>(); 076 // @NonNull 077 // private final InstanceReader instanceReader = new InstanceReader(); 078 @NonNull 079 private final URI source; 080 @NonNull 081 private final IJsonProblemHandler problemHandler; 082 /** 083 * Tracks the current parsing path for context-aware error reporting. 084 */ 085 @NonNull 086 private final PathTracker pathTracker = new PathTracker(); 087 088 /** 089 * Construct a new Module-aware JSON parser using the default problem handler. 090 * 091 * @param parser 092 * the JSON parser to parse with 093 * @param source 094 * the resource being parsed 095 * @throws IOException 096 * if an error occurred while reading the JSON 097 * @see DefaultJsonProblemHandler 098 */ 099 @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields") 100 public MetaschemaJsonReader( 101 @NonNull JsonParser parser, 102 @NonNull URI source) throws IOException { 103 this(parser, source, new DefaultJsonProblemHandler()); 104 } 105 106 /** 107 * Construct a new Module-aware JSON parser. 108 * 109 * @param parser 110 * the JSON parser to parse with 111 * @param source 112 * the resource being parsed 113 * @param problemHandler 114 * the problem handler implementation to use 115 * @throws IOException 116 * if an error occurred while reading the JSON 117 */ 118 @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields") 119 public MetaschemaJsonReader( 120 @NonNull JsonParser parser, 121 @NonNull URI source, 122 @NonNull IJsonProblemHandler problemHandler) throws IOException { 123 this.source = source; 124 this.problemHandler = problemHandler; 125 push(parser); 126 } 127 128 @SuppressWarnings("resource") 129 @NotOwning 130 @Override 131 public JsonParser getReader() { 132 return ObjectUtils.notNull(parserStack.peek()); 133 } 134 135 @Override 136 public URI getSource() { 137 return source; 138 } 139 // protected void analyzeParserStack(@NonNull String action) throws IOException 140 // { 141 // StringBuilder builder = new StringBuilder() 142 // .append("------\n"); 143 // 144 // for (JsonParser parser : parserStack) { 145 // JsonToken token = parser.getCurrentToken(); 146 // if (token == null) { 147 // LOGGER.info(String.format("Advancing parser: %s", parser.hashCode())); 148 // token = parser.nextToken(); 149 // } 150 // 151 // String name = parser.currentName(); 152 // builder.append(String.format("%s: %d: %s(%s)%s\n", 153 // action, 154 // parser.hashCode(), 155 // token.name(), 156 // name == null ? "" : name, 157 // JsonUtil.generateLocationMessage(parser))); 158 // } 159 // LOGGER.info(builder.toString()); 160 // } 161 162 @SuppressWarnings("resource") 163 private void push(@NonNull JsonParser parser) throws IOException { 164 assert !parser.equals(parserStack.peek()); 165 if (parser.getCurrentToken() == null) { 166 parser.nextToken(); 167 } 168 parserStack.push(parser); 169 } 170 171 @SuppressWarnings({ "resource", "PMD.CloseResource" }) 172 @NonNull 173 private JsonParser pop(@NonNull JsonParser parser) { 174 JsonParser old = parserStack.pop(); 175 assert parser.equals(old); 176 return ObjectUtils.notNull(parserStack.peek()); 177 } 178 179 @Override 180 public IJsonProblemHandler getProblemHandler() { 181 return problemHandler; 182 } 183 184 /** 185 * Build a validation context from the current parser state. 186 * 187 * @return a new validation context with current location and path 188 */ 189 @NonNull 190 private ValidationContext buildValidationContext() { 191 JsonParser parser = getReader(); 192 JsonLocation location = parser.currentLocation(); 193 IResourceLocation resourceLocation = JsonLocation.NA.equals(location) 194 ? SimpleResourceLocation.UNKNOWN 195 : SimpleResourceLocation.fromJsonLocation(location); 196 return ValidationContext.of(source, resourceLocation, pathTracker.getCurrentPath(), Format.JSON); 197 } 198 199 /** 200 * Read a JSON object value based on the provided definition. 201 * 202 * @param <T> 203 * the Java type of the bound object produced by this parser 204 * @param definition 205 * the Metaschema module definition that describes the node to parse 206 * @return the resulting parsed bound object 207 * @throws IOException 208 * if an error occurred while parsing the content 209 */ 210 @SuppressWarnings("unchecked") 211 @NonNull 212 public <T> T readObject(@NonNull IBoundDefinitionModelComplex definition) throws IOException { 213 T value = (T) definition.readItem(null, this); 214 if (value == null) { 215 throw new IOException(String.format("Failed to read object '%s'%s.", 216 definition.getDefinitionQName(), 217 JsonUtil.generateLocationMessage(getReader(), getSource()))); 218 } 219 return value; 220 } 221 222 /** 223 * Read a JSON property based on the provided definition. 224 * 225 * @param <T> 226 * the Java type of the bound object produced by this parser 227 * @param definition 228 * the Metaschema module definition that describes the node to parse 229 * @param expectedFieldName 230 * the name of the JSON field to parse 231 * @return the resulting parsed bound object 232 * @throws IOException 233 * if an error occurred while parsing the content 234 */ 235 @SuppressWarnings({ 236 "unchecked", 237 "PMD.CyclomaticComplexity" 238 }) 239 @NonNull 240 public <T> T readObjectRoot( 241 @NonNull IBoundDefinitionModelComplex definition, 242 @NonNull String expectedFieldName) throws IOException { 243 @SuppressWarnings("PMD.CloseResource") 244 JsonParser parser = getReader(); 245 URI resource = getSource(); 246 247 boolean hasStartObject = JsonToken.START_OBJECT.equals(parser.currentToken()); 248 if (hasStartObject) { 249 // advance past the start object 250 JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_OBJECT); 251 } 252 253 T retval = null; 254 JsonToken token; 255 while (!JsonToken.END_OBJECT.equals(token = parser.currentToken()) && token != null) { 256 if (!JsonToken.FIELD_NAME.equals(token)) { 257 throw new IOException(String.format("Expected FIELD_NAME token, found '%s'", token.toString())); 258 } 259 260 String propertyName = ObjectUtils.notNull(parser.currentName()); 261 if (expectedFieldName.equals(propertyName)) { 262 // process the object value, bound to the requested class 263 JsonUtil.assertAndAdvance(parser, resource, JsonToken.FIELD_NAME); 264 265 // stop now, since we found the field 266 retval = (T) definition.readItem(null, this); 267 break; 268 } 269 270 if (!getProblemHandler().handleUnknownProperty( 271 definition, 272 null, 273 propertyName, 274 this)) { 275 if (LOGGER.isWarnEnabled()) { 276 LOGGER.warn("Skipping unhandled JSON field '{}'{}.", propertyName, JsonUtil.toString(parser, resource)); 277 } 278 JsonUtil.skipNextValue(parser, resource); 279 } 280 } 281 282 if (hasStartObject) { 283 // advance past the end object 284 JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_OBJECT); 285 } 286 287 if (retval == null) { 288 throw new IOException(String.format("Failed to find property with name '%s'%s.", 289 expectedFieldName, 290 JsonUtil.generateLocationMessage(parser, resource))); 291 } 292 return retval; 293 } 294 295 // ================ 296 // Instance readers 297 // ================ 298 299 @Nullable 300 private Object readInstance( 301 @NonNull IBoundProperty<?> instance, 302 @NonNull IBoundObject parent) throws IOException { 303 return instance.readItem(parent, this); 304 } 305 306 @Nullable 307 private <T> Object readModelInstance( 308 @NonNull IBoundInstanceModel<T> instance, 309 @NonNull IBoundObject parent) throws IOException { 310 IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo(); 311 return collectionInfo.readItems(new ModelInstanceReadHandler<>(instance, parent)); 312 } 313 314 private Object readFieldValue( 315 @NonNull IBoundFieldValue instance, 316 @NonNull IBoundObject parent) throws IOException { 317 // handle the value key name case 318 return instance.readItem(parent, this); 319 } 320 321 @Nullable 322 private Object readObjectProperty( 323 @NonNull IBoundObject parent, 324 @NonNull IBoundProperty<?> property) throws IOException { 325 Object retval; 326 if (property instanceof IBoundInstanceModel) { 327 retval = readModelInstance((IBoundInstanceModel<?>) property, parent); 328 } else if (property instanceof IBoundInstance) { 329 retval = readInstance(property, parent); 330 } else { // IBoundFieldValue 331 retval = readFieldValue((IBoundFieldValue) property, parent); 332 } 333 return retval; 334 } 335 336 @Override 337 public Object readItemFlag(IBoundObject parentItem, IBoundInstanceFlag instance) throws IOException { 338 return readScalarItem(instance); 339 } 340 341 @Override 342 public Object readItemField(IBoundObject parentItem, IBoundInstanceModelFieldScalar instance) throws IOException { 343 return readScalarItem(instance); 344 } 345 346 @Override 347 public IBoundObject readItemField(IBoundObject parentItem, IBoundInstanceModelFieldComplex instance) 348 throws IOException { 349 return readFieldObject( 350 parentItem, 351 instance.getDefinition(), 352 instance.getJsonProperties(), 353 instance.getEffectiveJsonKey(), 354 getProblemHandler()); 355 } 356 357 @Override 358 public IBoundObject readItemField(IBoundObject parentItem, IBoundInstanceModelGroupedField instance) 359 throws IOException { 360 IJsonProblemHandler problemHandler = new GroupedInstanceProblemHandler(instance, getProblemHandler()); 361 IBoundDefinitionModelFieldComplex definition = instance.getDefinition(); 362 IBoundInstanceFlag jsonValueKeyFlag = definition.getJsonValueKeyFlagInstance(); 363 364 IJsonProblemHandler actualProblemHandler = jsonValueKeyFlag == null 365 ? problemHandler 366 : new JsomValueKeyProblemHandler(problemHandler, jsonValueKeyFlag); 367 368 return readComplexDefinitionObject( 369 parentItem, 370 definition, 371 instance.getEffectiveJsonKey(), 372 new PropertyBodyHandler(instance.getJsonProperties()), 373 actualProblemHandler); 374 } 375 376 @Override 377 public IBoundObject readItemField(IBoundObject parentItem, IBoundDefinitionModelFieldComplex definition) 378 throws IOException { 379 return readFieldObject( 380 parentItem, 381 definition, 382 definition.getJsonProperties(), 383 null, 384 getProblemHandler()); 385 } 386 387 @Override 388 public Object readItemFieldValue(IBoundObject parentItem, IBoundFieldValue fieldValue) throws IOException { 389 // read the field value's value 390 return checkMissingFieldValue(readScalarItem(fieldValue)); 391 } 392 393 @Nullable 394 private Object checkMissingFieldValue(Object value) { 395 if (value == null && LOGGER.isWarnEnabled()) { 396 LOGGER.atWarn().log("Missing property value{}", 397 JsonUtil.generateLocationMessage(getReader(), getSource())); 398 } 399 return value; 400 } 401 402 @Override 403 public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundInstanceModelAssembly instance) 404 throws IOException { 405 IBoundInstanceFlag jsonKey = instance.getJsonKey(); 406 IBoundDefinitionModelComplex definition = instance.getDefinition(); 407 return readComplexDefinitionObject( 408 parentItem, 409 definition, 410 jsonKey, 411 new PropertyBodyHandler(instance.getJsonProperties()), 412 getProblemHandler()); 413 } 414 415 @Override 416 public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundInstanceModelGroupedAssembly instance) 417 throws IOException { 418 return readComplexDefinitionObject( 419 parentItem, 420 instance.getDefinition(), 421 instance.getEffectiveJsonKey(), 422 new PropertyBodyHandler(instance.getJsonProperties()), 423 new GroupedInstanceProblemHandler(instance, getProblemHandler())); 424 } 425 426 @Override 427 public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundDefinitionModelAssembly definition) 428 throws IOException { 429 return readComplexDefinitionObject( 430 parentItem, 431 definition, 432 null, 433 new PropertyBodyHandler(definition.getJsonProperties()), 434 getProblemHandler()); 435 } 436 437 @NonNull 438 private Object readScalarItem(@NonNull IFeatureScalarItemValueHandler handler) 439 throws IOException { 440 return handler.getJavaTypeAdapter().parse(getReader(), getSource()); 441 } 442 443 @NonNull 444 private IBoundObject readFieldObject( 445 @Nullable IBoundObject parentItem, 446 @NonNull IBoundDefinitionModelFieldComplex definition, 447 @NonNull Map<String, IBoundProperty<?>> jsonProperties, 448 @Nullable IBoundInstanceFlag jsonKey, 449 @NonNull IJsonProblemHandler problemHandler) throws IOException { 450 IBoundInstanceFlag jsonValueKey = definition.getJsonValueKeyFlagInstance(); 451 IJsonProblemHandler actualProblemHandler = jsonValueKey == null 452 ? problemHandler 453 : new JsomValueKeyProblemHandler(problemHandler, jsonValueKey); 454 455 IBoundObject retval; 456 if (jsonProperties.isEmpty() && jsonValueKey == null) { 457 retval = readComplexDefinitionObject( 458 parentItem, 459 definition, 460 jsonKey, 461 (def, parent, problem) -> { 462 IBoundFieldValue fieldValue = definition.getFieldValue(); 463 Object item = readItemFieldValue(parent, fieldValue); 464 if (item != null) { 465 fieldValue.setValue(parent, item); 466 } 467 }, 468 actualProblemHandler); 469 470 } else { 471 retval = readComplexDefinitionObject( 472 parentItem, 473 definition, 474 jsonKey, 475 new PropertyBodyHandler(jsonProperties), 476 actualProblemHandler); 477 } 478 return retval; 479 } 480 481 @NonNull 482 private IBoundObject readComplexDefinitionObject( 483 @Nullable IBoundObject parentItem, 484 @NonNull IBoundDefinitionModelComplex definition, 485 @Nullable IBoundInstanceFlag jsonKey, 486 @NonNull DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler, 487 @NonNull IJsonProblemHandler problemHandler) throws IOException { 488 DefinitionBodyHandler<IBoundDefinitionModelComplex> actualBodyHandler = jsonKey == null 489 ? bodyHandler 490 : new JsonKeyBodyHandler(jsonKey, bodyHandler); 491 492 JsonLocation location = getReader().currentLocation(); 493 494 // construct the item 495 IBoundObject item = definition.newInstance( 496 JsonLocation.NA.equals(location) 497 ? null 498 : () -> SimpleResourceLocation.fromJsonLocation(ObjectUtils.requireNonNull(location))); 499 500 try { 501 // call pre-parse initialization hook 502 definition.callBeforeDeserialize(item, parentItem); 503 504 // read the property values 505 actualBodyHandler.accept(definition, item, problemHandler); 506 507 // call post-parse initialization hook 508 definition.callAfterDeserialize(item, parentItem); 509 } catch (BindingException ex) { 510 throw new IOException(ex); 511 } 512 513 return item; 514 } 515 516 @SuppressWarnings("resource") 517 @Override 518 public IBoundObject readChoiceGroupItem(IBoundObject parentItem, IBoundInstanceModelChoiceGroup instance) 519 throws IOException { 520 @SuppressWarnings("PMD.CloseResource") 521 JsonParser parser = getReader(); 522 ObjectNode node = parser.readValueAsTree(); 523 524 String discriminatorProperty = instance.getJsonDiscriminatorProperty(); 525 JsonNode discriminatorNode = node.get(discriminatorProperty); 526 if (discriminatorNode == null) { 527 throw new IllegalArgumentException(String.format( 528 "Unable to find discriminator property '%s' for object at '%s'.", 529 discriminatorProperty, 530 JsonUtil.toString(parser, getSource()))); 531 } 532 String discriminator = ObjectUtils.requireNonNull(discriminatorNode.asText()); 533 534 IBoundInstanceModelGroupedNamed actualInstance = instance.getGroupedModelInstance(discriminator); 535 assert actualInstance != null; 536 537 IBoundObject retval; 538 try (JsonParser newParser = node.traverse(parser.getCodec())) { 539 assert newParser != null; 540 push(newParser); 541 542 // get initial token 543 retval = actualInstance.readItem(parentItem, this); 544 assert newParser.currentToken() == null; 545 pop(newParser); 546 } 547 548 // advance the original parser to the next token 549 parser.nextToken(); 550 551 return retval; 552 } 553 554 private final class JsonKeyBodyHandler implements DefinitionBodyHandler<IBoundDefinitionModelComplex> { 555 @NonNull 556 private final IBoundInstanceFlag jsonKey; 557 @NonNull 558 private final DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler; 559 560 private JsonKeyBodyHandler( 561 @NonNull IBoundInstanceFlag jsonKey, 562 @NonNull DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler) { 563 this.jsonKey = jsonKey; 564 this.bodyHandler = bodyHandler; 565 } 566 567 @Override 568 public void accept( 569 IBoundDefinitionModelComplex definition, 570 IBoundObject parent, 571 IJsonProblemHandler problemHandler) 572 throws IOException { 573 @SuppressWarnings("PMD.CloseResource") 574 JsonParser parser = getReader(); 575 URI resource = getSource(); 576 JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME); 577 578 // the field will be the JSON key 579 String key = ObjectUtils.notNull(parser.currentName()); 580 try { 581 Object value = jsonKey.getDefinition().getJavaTypeAdapter().parse(key); 582 jsonKey.setValue(parent, ObjectUtils.notNull(value.toString())); 583 } catch (IllegalArgumentException ex) { 584 throw new IOException( 585 String.format("Malformed data '%s'%s. %s", 586 key, 587 JsonUtil.generateLocationMessage(parser, resource), 588 ex.getLocalizedMessage()), 589 ex); 590 } 591 592 // skip to the next token 593 parser.nextToken(); 594 // JsonUtil.assertCurrent(parser, JsonToken.START_OBJECT); 595 596 // // advance past the JSON key's start object 597 // JsonUtil.assertAndAdvance(parser, JsonToken.START_OBJECT); 598 599 // read the property values 600 bodyHandler.accept(definition, parent, problemHandler); 601 602 // // advance past the JSON key's end object 603 // JsonUtil.assertAndAdvance(parser, JsonToken.END_OBJECT); 604 } 605 } 606 607 /** 608 * Capture the next JSON property value as a {@link JsonNode} tree and advance 609 * the parser past it. This method handles the parser being positioned at either 610 * a {@link JsonToken#FIELD_NAME} or a value token. After this method returns, 611 * the parser is positioned at the token immediately following the captured 612 * value. 613 * 614 * @param parser 615 * the JSON parser 616 * @param resource 617 * the resource being parsed 618 * @return the captured value as a {@link JsonNode} 619 * @throws IOException 620 * if an error occurred while reading 621 */ 622 @SuppressWarnings({ 623 "resource", // parser not owned 624 "PMD.CyclomaticComplexity" // acceptable 625 }) 626 @NonNull 627 private static JsonNode capturePropertyValue( 628 @NonNull JsonParser parser, 629 @NonNull URI resource) throws IOException { 630 631 // skip the field name if present 632 if (parser.currentToken() == JsonToken.FIELD_NAME) { 633 parser.nextToken(); 634 } 635 636 JsonNode retval = buildJsonValue(parser); 637 // advance past the current value token (or past END_OBJECT/END_ARRAY 638 // for containers which are left at the closing token by buildJsonValue) 639 parser.nextToken(); 640 return retval; 641 } 642 643 /** 644 * Recursively build a {@link JsonNode} from the current parser position. For 645 * scalar values, reads the current token's value. For containers (objects and 646 * arrays), recursively reads all nested content. After this method returns for 647 * a container, the parser is positioned at the container's closing token 648 * ({@code END_OBJECT} or {@code END_ARRAY}). 649 * 650 * @param parser 651 * the parser positioned at a value token 652 * @return the value as a JsonNode 653 * @throws IOException 654 * if an error occurred while reading 655 */ 656 @SuppressWarnings("PMD.CyclomaticComplexity") // token type switch 657 @NonNull 658 private static JsonNode buildJsonValue(@NonNull JsonParser parser) throws IOException { 659 JsonNodeFactory nodeFactory = JsonNodeFactory.instance; 660 JsonToken token = parser.currentToken(); 661 662 switch (token) { 663 case START_OBJECT: { 664 ObjectNode obj = nodeFactory.objectNode(); 665 while (parser.nextToken() != JsonToken.END_OBJECT) { 666 String fieldName = ObjectUtils.requireNonNull(parser.currentName()); 667 parser.nextToken(); // advance to value 668 obj.set(fieldName, buildJsonValue(parser)); 669 } 670 // parser is now at END_OBJECT 671 return obj; 672 } 673 case START_ARRAY: { 674 com.fasterxml.jackson.databind.node.ArrayNode arr = nodeFactory.arrayNode(); 675 while (parser.nextToken() != JsonToken.END_ARRAY) { 676 arr.add(buildJsonValue(parser)); 677 } 678 // parser is now at END_ARRAY 679 return arr; 680 } 681 case VALUE_STRING: 682 return nodeFactory.textNode(ObjectUtils.requireNonNull(parser.getText())); 683 case VALUE_NUMBER_INT: 684 return nodeFactory.numberNode(parser.getBigIntegerValue()); 685 case VALUE_NUMBER_FLOAT: 686 return nodeFactory.numberNode(parser.getDecimalValue()); 687 case VALUE_TRUE: 688 return nodeFactory.booleanNode(true); 689 case VALUE_FALSE: 690 return nodeFactory.booleanNode(false); 691 case VALUE_NULL: 692 return nodeFactory.nullNode(); 693 default: 694 throw new IOException( 695 String.format("Unexpected token '%s' when capturing JSON value.", token)); 696 } 697 } 698 699 private final class PropertyBodyHandler implements DefinitionBodyHandler<IBoundDefinitionModelComplex> { 700 @NonNull 701 private final Map<String, IBoundProperty<?>> jsonProperties; 702 703 private PropertyBodyHandler(@NonNull Map<String, IBoundProperty<?>> jsonProperties) { 704 this.jsonProperties = jsonProperties; 705 } 706 707 @Override 708 public void accept( 709 IBoundDefinitionModelComplex definition, 710 IBoundObject parent, 711 IJsonProblemHandler problemHandler) 712 throws IOException { 713 @SuppressWarnings("PMD.CloseResource") 714 JsonParser parser = getReader(); 715 URI resource = getSource(); 716 717 // Track path for error messages 718 pathTracker.push(definition.getEffectiveName()); 719 720 try { 721 // advance past the start object 722 JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_OBJECT); 723 724 // make a copy, since we use the remaining values to initialize default values 725 Map<String, IBoundProperty<?>> remainingInstances = new HashMap<>(jsonProperties); // NOPMD not concurrent 726 727 // Determine if this definition supports any content capture 728 IBoundInstanceModelAny boundAny = resolveAnyInstance(definition); 729 730 // Accumulator for unmodeled properties when any instance is present 731 ObjectNode anyAccumulator = null; 732 733 // handle each property 734 while (JsonToken.FIELD_NAME.equals(parser.currentToken())) { 735 736 // the parser's current token should be the JSON field name 737 String propertyName = ObjectUtils.notNull(parser.currentName()); 738 if (LOGGER.isTraceEnabled()) { 739 LOGGER.trace("reading property {}", propertyName); 740 } 741 742 IBoundProperty<?> property = remainingInstances.get(propertyName); 743 744 boolean handled = false; 745 if (property != null) { 746 // advance past the field name 747 parser.nextToken(); 748 749 Object value = readObjectProperty(parent, property); 750 if (value != null) { 751 property.setValue(parent, value); 752 } 753 754 // mark handled 755 remainingInstances.remove(propertyName); 756 handled = true; 757 } 758 759 if (!handled && !problemHandler.handleUnknownProperty( 760 definition, 761 parent, 762 propertyName, 763 MetaschemaJsonReader.this)) { 764 if (boundAny != null) { 765 // Capture the unmodeled property value into the any accumulator. 766 // Advance past the field name to the value, then skip the value 767 // using the same pattern as skipNextValue, but capture it. 768 JsonNode value = capturePropertyValue(parser, resource); 769 if (anyAccumulator == null) { 770 anyAccumulator = new ObjectNode(JsonNodeFactory.instance); 771 } 772 anyAccumulator.set(propertyName, value); 773 } else { 774 if (LOGGER.isWarnEnabled()) { 775 LOGGER.warn("Skipping unhandled JSON field '{}' {}.", 776 propertyName, JsonUtil.toString(parser, resource)); 777 } 778 JsonUtil.assertAndAdvance(parser, resource, JsonToken.FIELD_NAME); 779 JsonUtil.skipNextValue(parser, resource); 780 } 781 } 782 783 // the current token will be either the next instance field name or the end of 784 // the parent object 785 JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME, JsonToken.END_OBJECT); 786 } 787 788 // Set any captured content on the parent object 789 if (boundAny != null && anyAccumulator != null && !anyAccumulator.isEmpty()) { 790 boundAny.setAnyContent(parent, new JsonAnyContent(anyAccumulator)); 791 } 792 793 // Build validation context with current location and path 794 ValidationContext context = buildValidationContext(); 795 problemHandler.handleMissingInstances( 796 definition, 797 parent, 798 ObjectUtils.notNull(remainingInstances.values()), 799 context); 800 801 // advance past the end object 802 JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_OBJECT); 803 } finally { 804 pathTracker.pop(); 805 } 806 } 807 808 /** 809 * Resolve the bound any instance from the given definition, if available. 810 * 811 * @param definition 812 * the complex definition to check 813 * @return the bound any instance, or {@code null} if the definition does not 814 * support any content 815 */ 816 @Nullable 817 private IBoundInstanceModelAny resolveAnyInstance( 818 @NonNull IBoundDefinitionModelComplex definition) { 819 if (definition instanceof IBoundDefinitionModelAssembly) { 820 IAnyInstance anyInstance 821 = ((IBoundDefinitionModelAssembly) definition).getModelContainer().getAnyInstance(); 822 if (anyInstance instanceof IBoundInstanceModelAny) { 823 return (IBoundInstanceModelAny) anyInstance; 824 } 825 } 826 return null; 827 } 828 } 829 830 private static final class GroupedInstanceProblemHandler implements IJsonProblemHandler { 831 @NonNull 832 private final IBoundInstanceModelGroupedNamed instance; 833 @NonNull 834 private final IJsonProblemHandler delegate; 835 836 private GroupedInstanceProblemHandler( 837 @NonNull IBoundInstanceModelGroupedNamed instance, 838 @NonNull IJsonProblemHandler delegate) { 839 this.instance = instance; 840 this.delegate = delegate; 841 } 842 843 @Override 844 public void handleMissingInstances( 845 IBoundDefinitionModelComplex parentDefinition, 846 IBoundObject targetObject, 847 Collection<? extends IBoundProperty<?>> unhandledInstances) throws IOException { 848 delegate.handleMissingInstances(parentDefinition, targetObject, unhandledInstances); 849 } 850 851 @Override 852 public void handleMissingInstances( 853 IBoundDefinitionModelComplex parentDefinition, 854 IBoundObject targetObject, 855 Collection<? extends IBoundProperty<?>> unhandledInstances, 856 ValidationContext context) throws IOException { 857 delegate.handleMissingInstances(parentDefinition, targetObject, unhandledInstances, context); 858 } 859 860 @Override 861 public boolean handleUnknownProperty( 862 IBoundDefinitionModelComplex definition, 863 IBoundObject parentItem, 864 String fieldName, 865 IJsonParsingContext parsingContext) throws IOException { 866 boolean retval; 867 if (instance.getParentContainer().getJsonDiscriminatorProperty().equals(fieldName)) { 868 JsonUtil.skipNextValue(parsingContext.getReader(), parsingContext.getSource()); 869 retval = true; 870 } else { 871 retval = delegate.handleUnknownProperty(definition, parentItem, fieldName, parsingContext); 872 } 873 return retval; 874 } 875 } 876 877 private final class JsomValueKeyProblemHandler implements IJsonProblemHandler { 878 @NonNull 879 private final IJsonProblemHandler delegate; 880 @NonNull 881 private final IBoundInstanceFlag jsonValueKeyFlag; 882 private boolean foundJsonValueKey; // false 883 884 private JsomValueKeyProblemHandler( 885 @NonNull IJsonProblemHandler delegate, 886 @NonNull IBoundInstanceFlag jsonValueKeyFlag) { 887 this.delegate = delegate; 888 this.jsonValueKeyFlag = jsonValueKeyFlag; 889 } 890 891 @Override 892 public void handleMissingInstances( 893 IBoundDefinitionModelComplex parentDefinition, 894 IBoundObject targetObject, 895 Collection<? extends IBoundProperty<?>> unhandledInstances) throws IOException { 896 delegate.handleMissingInstances(parentDefinition, targetObject, unhandledInstances); 897 } 898 899 @Override 900 public void handleMissingInstances( 901 IBoundDefinitionModelComplex parentDefinition, 902 IBoundObject targetObject, 903 Collection<? extends IBoundProperty<?>> unhandledInstances, 904 ValidationContext context) throws IOException { 905 delegate.handleMissingInstances(parentDefinition, targetObject, unhandledInstances, context); 906 } 907 908 @Override 909 public boolean handleUnknownProperty( 910 IBoundDefinitionModelComplex definition, 911 IBoundObject parentItem, 912 String fieldName, 913 IJsonParsingContext parsingContext) throws IOException { 914 boolean retval; 915 if (foundJsonValueKey) { 916 retval = delegate.handleUnknownProperty(definition, parentItem, fieldName, parsingContext); 917 } else { 918 @SuppressWarnings("PMD.CloseResource") 919 JsonParser parser = parsingContext.getReader(); 920 URI resource = parsingContext.getSource(); 921 922 // handle JSON value key 923 String key = ObjectUtils.notNull(parser.currentName()); 924 try { 925 Object keyValue = jsonValueKeyFlag.getJavaTypeAdapter().parse(key); 926 jsonValueKeyFlag.setValue(ObjectUtils.notNull(parentItem), keyValue); 927 } catch (IllegalArgumentException ex) { 928 throw new IOException( 929 String.format("Malformed data '%s'%s. %s", 930 key, 931 JsonUtil.generateLocationMessage(parser, resource), 932 ex.getLocalizedMessage()), 933 ex); 934 } 935 // advance past the field name 936 JsonUtil.assertAndAdvance(parser, resource, JsonToken.FIELD_NAME); 937 938 IBoundFieldValue fieldValue = ((IBoundDefinitionModelFieldComplex) definition).getFieldValue(); 939 Object value = readItemFieldValue(ObjectUtils.notNull(parentItem), fieldValue); 940 if (value != null) { 941 fieldValue.setValue(ObjectUtils.notNull(parentItem), value); 942 } 943 944 retval = foundJsonValueKey = true; 945 } 946 return retval; 947 } 948 } 949 950 private class ModelInstanceReadHandler<ITEM> 951 extends AbstractModelInstanceReadHandler<ITEM> { 952 953 protected ModelInstanceReadHandler( 954 @NonNull IBoundInstanceModel<ITEM> instance, 955 @NonNull IBoundObject parentItem) { 956 super(instance, parentItem); 957 } 958 959 @Override 960 public List<ITEM> readList() throws IOException { 961 @SuppressWarnings("PMD.CloseResource") 962 JsonParser parser = getReader(); 963 URI resource = getSource(); 964 965 List<ITEM> items = new LinkedList<>(); 966 switch (parser.currentToken()) { 967 case START_ARRAY: 968 // this is an array, we need to parse the array wrapper then each item 969 JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_ARRAY); 970 971 // parse items 972 while (!JsonToken.END_ARRAY.equals(parser.currentToken())) { 973 items.add(readItem()); 974 } 975 976 // this is the other side of the array wrapper, advance past it 977 JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_ARRAY); 978 break; 979 case VALUE_NULL: 980 JsonUtil.assertAndAdvance(parser, resource, JsonToken.VALUE_NULL); 981 break; 982 default: 983 // this is a singleton, just parse the value as a single item 984 items.add(readItem()); 985 break; 986 } 987 return items; 988 } 989 990 @Override 991 public Map<String, ITEM> readMap() throws IOException { 992 @SuppressWarnings("PMD.CloseResource") 993 JsonParser parser = getReader(); 994 URI resource = getSource(); 995 996 IBoundInstanceModel<?> instance = getCollectionInfo().getInstance(); 997 998 @SuppressWarnings("PMD.UseConcurrentHashMap") 999 Map<String, ITEM> items = new LinkedHashMap<>(); 1000 1001 // A map value is always wrapped in a START_OBJECT, since fields are used for 1002 // the keys 1003 JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_OBJECT); 1004 1005 // process all map items 1006 while (!JsonToken.END_OBJECT.equals(parser.currentToken())) { 1007 1008 // a map item will always start with a FIELD_NAME, since this represents the key 1009 JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME); 1010 1011 // get the object, since it must have a JSON key 1012 ITEM item = readItem(); 1013 if (item == null) { 1014 throw new IOException(String.format("Null object encountered'%s.", 1015 JsonUtil.generateLocationMessage(parser, resource))); 1016 } 1017 1018 // lookup the key 1019 IBoundInstanceFlag jsonKey = instance.getItemJsonKey(item); 1020 assert jsonKey != null; 1021 1022 Object keyValue = jsonKey.getValue(item); 1023 if (keyValue == null) { 1024 throw new IOException(String.format("Null value for json-key for definition '%s'", 1025 jsonKey.getContainingDefinition().toCoordinates())); 1026 } 1027 String key; 1028 try { 1029 key = jsonKey.getJavaTypeAdapter().asString(keyValue); 1030 } catch (IllegalArgumentException ex) { 1031 throw new IOException( 1032 String.format("Malformed data '%s'%s. %s", 1033 keyValue, 1034 JsonUtil.generateLocationMessage(parser, resource), 1035 ex.getLocalizedMessage()), 1036 ex); 1037 } 1038 items.put(key, item); 1039 1040 // the next item will be a FIELD_NAME, or we will encounter an END_OBJECT if all 1041 // items have been 1042 // read 1043 JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME, JsonToken.END_OBJECT); 1044 } 1045 1046 // A map value will always end with an end object, which needs to be consumed 1047 JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_OBJECT); 1048 1049 return items; 1050 } 1051 1052 @Override 1053 public ITEM readItem() throws IOException { 1054 IBoundInstanceModel<ITEM> instance = getCollectionInfo().getInstance(); 1055 return instance.readItem(getParentObject(), MetaschemaJsonReader.this); 1056 } 1057 } 1058 1059 @FunctionalInterface 1060 private interface DefinitionBodyHandler<DEF extends IBoundDefinitionModelComplex> { 1061 void accept( 1062 @NonNull DEF definition, 1063 @NonNull IBoundObject parent, 1064 @NonNull IJsonProblemHandler problemHandler) throws IOException; 1065 } 1066}