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