1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.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.node.ObjectNode;
13  
14  import org.apache.logging.log4j.LogManager;
15  import org.apache.logging.log4j.Logger;
16  import org.eclipse.jdt.annotation.NotOwning;
17  
18  import java.io.IOException;
19  import java.net.URI;
20  import java.util.Collection;
21  import java.util.Deque;
22  import java.util.HashMap;
23  import java.util.LinkedHashMap;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.Map;
27  
28  import dev.metaschema.core.model.IBoundObject;
29  import dev.metaschema.core.model.IResourceLocation;
30  import dev.metaschema.core.model.SimpleResourceLocation;
31  import dev.metaschema.core.model.util.JsonUtil;
32  import dev.metaschema.core.util.ObjectUtils;
33  import dev.metaschema.databind.io.BindingException;
34  import dev.metaschema.databind.io.Format;
35  import dev.metaschema.databind.io.PathTracker;
36  import dev.metaschema.databind.io.ValidationContext;
37  import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
38  import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
39  import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
40  import dev.metaschema.databind.model.IBoundFieldValue;
41  import dev.metaschema.databind.model.IBoundInstance;
42  import dev.metaschema.databind.model.IBoundInstanceFlag;
43  import dev.metaschema.databind.model.IBoundInstanceModel;
44  import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
45  import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
46  import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
47  import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
48  import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
49  import dev.metaschema.databind.model.IBoundInstanceModelGroupedField;
50  import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
51  import dev.metaschema.databind.model.IBoundProperty;
52  import dev.metaschema.databind.model.info.AbstractModelInstanceReadHandler;
53  import dev.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
54  import dev.metaschema.databind.model.info.IItemReadHandler;
55  import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
56  import edu.umd.cs.findbugs.annotations.NonNull;
57  import edu.umd.cs.findbugs.annotations.Nullable;
58  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
59  
60  /**
61   * Supports reading JSON-based Metaschema module instances.
62   */
63  @SuppressWarnings({
64      "PMD.CouplingBetweenObjects",
65      "PMD.GodClass"
66  })
67  public class MetaschemaJsonReader
68      implements IJsonParsingContext, IItemReadHandler {
69    private static final Logger LOGGER = LogManager.getLogger(MetaschemaJsonReader.class);
70  
71    @NonNull
72    private final Deque<JsonParser> parserStack = new LinkedList<>();
73    // @NonNull
74    // private final InstanceReader instanceReader = new InstanceReader();
75    @NonNull
76    private final URI source;
77    @NonNull
78    private final IJsonProblemHandler problemHandler;
79    /**
80     * Tracks the current parsing path for context-aware error reporting.
81     */
82    @NonNull
83    private final PathTracker pathTracker = new PathTracker();
84  
85    /**
86     * Construct a new Module-aware JSON parser using the default problem handler.
87     *
88     * @param parser
89     *          the JSON parser to parse with
90     * @param source
91     *          the resource being parsed
92     * @throws IOException
93     *           if an error occurred while reading the JSON
94     * @see DefaultJsonProblemHandler
95     */
96    @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
97    public MetaschemaJsonReader(
98        @NonNull JsonParser parser,
99        @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 }