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