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