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