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) {
369     if (value == null && LOGGER.isWarnEnabled()) {
370       LOGGER.atWarn().log("Missing property value{}",
371           JsonUtil.generateLocationMessage(getReader(), getSource()));
372     }
373     return value;
374   }
375 
376   @Override
377   public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundInstanceModelAssembly instance)
378       throws IOException {
379     IBoundInstanceFlag jsonKey = instance.getJsonKey();
380     IBoundDefinitionModelComplex definition = instance.getDefinition();
381     return readComplexDefinitionObject(
382         parentItem,
383         definition,
384         jsonKey,
385         new PropertyBodyHandler(instance.getJsonProperties()),
386         getProblemHandler());
387   }
388 
389   @Override
390   public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundInstanceModelGroupedAssembly instance)
391       throws IOException {
392     return readComplexDefinitionObject(
393         parentItem,
394         instance.getDefinition(),
395         instance.getEffectiveJsonKey(),
396         new PropertyBodyHandler(instance.getJsonProperties()),
397         new GroupedInstanceProblemHandler(instance, getProblemHandler()));
398   }
399 
400   @Override
401   public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundDefinitionModelAssembly definition)
402       throws IOException {
403     return readComplexDefinitionObject(
404         parentItem,
405         definition,
406         null,
407         new PropertyBodyHandler(definition.getJsonProperties()),
408         getProblemHandler());
409   }
410 
411   @NonNull
412   private Object readScalarItem(@NonNull IFeatureScalarItemValueHandler handler)
413       throws IOException {
414     return handler.getJavaTypeAdapter().parse(getReader(), getSource());
415   }
416 
417   @NonNull
418   private IBoundObject readFieldObject(
419       @Nullable IBoundObject parentItem,
420       @NonNull IBoundDefinitionModelFieldComplex definition,
421       @NonNull Map<String, IBoundProperty<?>> jsonProperties,
422       @Nullable IBoundInstanceFlag jsonKey,
423       @NonNull IJsonProblemHandler problemHandler) throws IOException {
424     IBoundInstanceFlag jsonValueKey = definition.getJsonValueKeyFlagInstance();
425     IJsonProblemHandler actualProblemHandler = jsonValueKey == null
426         ? problemHandler
427         : new JsomValueKeyProblemHandler(problemHandler, jsonValueKey);
428 
429     IBoundObject retval;
430     if (jsonProperties.isEmpty() && jsonValueKey == null) {
431       retval = readComplexDefinitionObject(
432           parentItem,
433           definition,
434           jsonKey,
435           (def, parent, problem) -> {
436             IBoundFieldValue fieldValue = definition.getFieldValue();
437             Object item = readItemFieldValue(parent, fieldValue);
438             if (item != null) {
439               fieldValue.setValue(parent, item);
440             }
441           },
442           actualProblemHandler);
443 
444     } else {
445       retval = readComplexDefinitionObject(
446           parentItem,
447           definition,
448           jsonKey,
449           new PropertyBodyHandler(jsonProperties),
450           actualProblemHandler);
451     }
452     return retval;
453   }
454 
455   @NonNull
456   private IBoundObject readComplexDefinitionObject(
457       @Nullable IBoundObject parentItem,
458       @NonNull IBoundDefinitionModelComplex definition,
459       @Nullable IBoundInstanceFlag jsonKey,
460       @NonNull DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler,
461       @NonNull IJsonProblemHandler problemHandler) throws IOException {
462     DefinitionBodyHandler<IBoundDefinitionModelComplex> actualBodyHandler = jsonKey == null
463         ? bodyHandler
464         : new JsonKeyBodyHandler(jsonKey, bodyHandler);
465 
466     @SuppressWarnings("PMD.CloseResource")
467     JsonLocation location = getReader().currentLocation();
468 
469     // construct the item
470     IBoundObject item = definition.newInstance(
471         JsonLocation.NA.equals(location)
472             ? null
473             : () -> new MetaschemaData(ObjectUtils.requireNonNull(location)));
474 
475     try {
476       // call pre-parse initialization hook
477       definition.callBeforeDeserialize(item, parentItem);
478 
479       // read the property values
480       actualBodyHandler.accept(definition, item, problemHandler);
481 
482       // call post-parse initialization hook
483       definition.callAfterDeserialize(item, parentItem);
484     } catch (BindingException ex) {
485       throw new IOException(ex);
486     }
487 
488     return item;
489   }
490 
491   @SuppressWarnings("resource")
492   @Override
493   public IBoundObject readChoiceGroupItem(IBoundObject parentItem, IBoundInstanceModelChoiceGroup instance)
494       throws IOException {
495     @SuppressWarnings("PMD.CloseResource")
496     JsonParser parser = getReader();
497     ObjectNode node = parser.readValueAsTree();
498 
499     String discriminatorProperty = instance.getJsonDiscriminatorProperty();
500     JsonNode discriminatorNode = node.get(discriminatorProperty);
501     if (discriminatorNode == null) {
502       throw new IllegalArgumentException(String.format(
503           "Unable to find discriminator property '%s' for object at '%s'.",
504           discriminatorProperty,
505           JsonUtil.toString(parser, getSource())));
506     }
507     String discriminator = ObjectUtils.requireNonNull(discriminatorNode.asText());
508 
509     IBoundInstanceModelGroupedNamed actualInstance = instance.getGroupedModelInstance(discriminator);
510     assert actualInstance != null;
511 
512     IBoundObject retval;
513     try (JsonParser newParser = node.traverse(parser.getCodec())) {
514       assert newParser != null;
515       push(newParser);
516 
517       // get initial token
518       retval = actualInstance.readItem(parentItem, this);
519       assert newParser.currentToken() == null;
520       pop(newParser);
521     }
522 
523     // advance the original parser to the next token
524     parser.nextToken();
525 
526     return retval;
527   }
528 
529   private final class JsonKeyBodyHandler implements DefinitionBodyHandler<IBoundDefinitionModelComplex> {
530     @NonNull
531     private final IBoundInstanceFlag jsonKey;
532     @NonNull
533     private final DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler;
534 
535     private JsonKeyBodyHandler(
536         @NonNull IBoundInstanceFlag jsonKey,
537         @NonNull DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler) {
538       this.jsonKey = jsonKey;
539       this.bodyHandler = bodyHandler;
540     }
541 
542     @Override
543     public void accept(
544         IBoundDefinitionModelComplex definition,
545         IBoundObject parent,
546         IJsonProblemHandler problemHandler)
547         throws IOException {
548       @SuppressWarnings("PMD.CloseResource")
549       JsonParser parser = getReader();
550       URI resource = getSource();
551       JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME);
552 
553       // the field will be the JSON key
554       String key = ObjectUtils.notNull(parser.currentName());
555       try {
556         Object value = jsonKey.getDefinition().getJavaTypeAdapter().parse(key);
557         jsonKey.setValue(parent, ObjectUtils.notNull(value.toString()));
558       } catch (IllegalArgumentException ex) {
559         throw new IOException(
560             String.format("Malformed data '%s'%s. %s",
561                 key,
562                 JsonUtil.generateLocationMessage(parser, resource),
563                 ex.getLocalizedMessage()),
564             ex);
565       }
566 
567       // skip to the next token
568       parser.nextToken();
569       // JsonUtil.assertCurrent(parser, JsonToken.START_OBJECT);
570 
571       // // advance past the JSON key's start object
572       // JsonUtil.assertAndAdvance(parser, JsonToken.START_OBJECT);
573 
574       // read the property values
575       bodyHandler.accept(definition, parent, problemHandler);
576 
577       // // advance past the JSON key's end object
578       // JsonUtil.assertAndAdvance(parser, JsonToken.END_OBJECT);
579     }
580   }
581 
582   private final class PropertyBodyHandler implements DefinitionBodyHandler<IBoundDefinitionModelComplex> {
583     @NonNull
584     private final Map<String, IBoundProperty<?>> jsonProperties;
585 
586     private PropertyBodyHandler(@NonNull Map<String, IBoundProperty<?>> jsonProperties) {
587       this.jsonProperties = jsonProperties;
588     }
589 
590     @Override
591     public void accept(
592         IBoundDefinitionModelComplex definition,
593         IBoundObject parent,
594         IJsonProblemHandler problemHandler)
595         throws IOException {
596       @SuppressWarnings("PMD.CloseResource")
597       JsonParser parser = getReader();
598       URI resource = getSource();
599 
600       // advance past the start object
601       JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_OBJECT);
602 
603       // make a copy, since we use the remaining values to initialize default values
604       Map<String, IBoundProperty<?>> remainingInstances = new HashMap<>(jsonProperties); // NOPMD not concurrent
605 
606       // handle each property
607       while (JsonToken.FIELD_NAME.equals(parser.currentToken())) {
608 
609         // the parser's current token should be the JSON field name
610         String propertyName = ObjectUtils.notNull(parser.currentName());
611         if (LOGGER.isTraceEnabled()) {
612           LOGGER.trace("reading property {}", propertyName);
613         }
614 
615         IBoundProperty<?> property = remainingInstances.get(propertyName);
616 
617         boolean handled = false;
618         if (property != null) {
619           // advance past the field name
620           parser.nextToken();
621 
622           Object value = readObjectProperty(parent, property);
623           if (value != null) {
624             property.setValue(parent, value);
625           }
626 
627           // mark handled
628           remainingInstances.remove(propertyName);
629           handled = true;
630         }
631 
632         if (!handled && !problemHandler.handleUnknownProperty(
633             definition,
634             parent,
635             propertyName,
636             MetaschemaJsonReader.this)) {
637           if (LOGGER.isWarnEnabled()) {
638             LOGGER.warn("Skipping unhandled JSON field '{}' {}.", propertyName, JsonUtil.toString(parser, resource));
639           }
640           JsonUtil.assertAndAdvance(parser, resource, JsonToken.FIELD_NAME);
641           JsonUtil.skipNextValue(parser, resource);
642         }
643 
644         // the current token will be either the next instance field name or the end of
645         // the parent object
646         JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME, JsonToken.END_OBJECT);
647       }
648 
649       problemHandler.handleMissingInstances(
650           definition,
651           parent,
652           ObjectUtils.notNull(remainingInstances.values()));
653 
654       // advance past the end object
655       JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_OBJECT);
656     }
657   }
658 
659   private static final class GroupedInstanceProblemHandler implements IJsonProblemHandler {
660     @NonNull
661     private final IBoundInstanceModelGroupedNamed instance;
662     @NonNull
663     private final IJsonProblemHandler delegate;
664 
665     private GroupedInstanceProblemHandler(
666         @NonNull IBoundInstanceModelGroupedNamed instance,
667         @NonNull IJsonProblemHandler delegate) {
668       this.instance = instance;
669       this.delegate = delegate;
670     }
671 
672     @Override
673     public void handleMissingInstances(
674         IBoundDefinitionModelComplex parentDefinition,
675         IBoundObject targetObject,
676         Collection<? extends IBoundProperty<?>> unhandledInstances) throws IOException {
677       delegate.handleMissingInstances(parentDefinition, targetObject, unhandledInstances);
678     }
679 
680     @Override
681     public boolean handleUnknownProperty(
682         IBoundDefinitionModelComplex definition,
683         IBoundObject parentItem,
684         String fieldName,
685         IJsonParsingContext parsingContext) throws IOException {
686       boolean retval;
687       if (instance.getParentContainer().getJsonDiscriminatorProperty().equals(fieldName)) {
688         JsonUtil.skipNextValue(parsingContext.getReader(), parsingContext.getSource());
689         retval = true;
690       } else {
691         retval = delegate.handleUnknownProperty(definition, parentItem, fieldName, parsingContext);
692       }
693       return retval;
694     }
695   }
696 
697   private final class JsomValueKeyProblemHandler implements IJsonProblemHandler {
698     @NonNull
699     private final IJsonProblemHandler delegate;
700     @NonNull
701     private final IBoundInstanceFlag jsonValueKeyFlag;
702     private boolean foundJsonValueKey; // false
703 
704     private JsomValueKeyProblemHandler(
705         @NonNull IJsonProblemHandler delegate,
706         @NonNull IBoundInstanceFlag jsonValueKeyFlag) {
707       this.delegate = delegate;
708       this.jsonValueKeyFlag = jsonValueKeyFlag;
709     }
710 
711     @Override
712     public void handleMissingInstances(
713         IBoundDefinitionModelComplex parentDefinition,
714         IBoundObject targetObject,
715         Collection<? extends IBoundProperty<?>> unhandledInstances) throws IOException {
716       delegate.handleMissingInstances(parentDefinition, targetObject, unhandledInstances);
717     }
718 
719     @Override
720     public boolean handleUnknownProperty(
721         IBoundDefinitionModelComplex definition,
722         IBoundObject parentItem,
723         String fieldName,
724         IJsonParsingContext parsingContext) throws IOException {
725       boolean retval;
726       if (foundJsonValueKey) {
727         retval = delegate.handleUnknownProperty(definition, parentItem, fieldName, parsingContext);
728       } else {
729         @SuppressWarnings("PMD.CloseResource")
730         JsonParser parser = parsingContext.getReader();
731         URI resource = parsingContext.getSource();
732 
733         // handle JSON value key
734         String key = ObjectUtils.notNull(parser.currentName());
735         try {
736           Object keyValue = jsonValueKeyFlag.getJavaTypeAdapter().parse(key);
737           jsonValueKeyFlag.setValue(ObjectUtils.notNull(parentItem), keyValue);
738         } catch (IllegalArgumentException ex) {
739           throw new IOException(
740               String.format("Malformed data '%s'%s. %s",
741                   key,
742                   JsonUtil.generateLocationMessage(parser, resource),
743                   ex.getLocalizedMessage()),
744               ex);
745         }
746         // advance past the field name
747         JsonUtil.assertAndAdvance(parser, resource, JsonToken.FIELD_NAME);
748 
749         IBoundFieldValue fieldValue = ((IBoundDefinitionModelFieldComplex) definition).getFieldValue();
750         Object value = readItemFieldValue(ObjectUtils.notNull(parentItem), fieldValue);
751         if (value != null) {
752           fieldValue.setValue(ObjectUtils.notNull(parentItem), value);
753         }
754 
755         retval = foundJsonValueKey = true;
756       }
757       return retval;
758     }
759   }
760 
761   private class ModelInstanceReadHandler<ITEM>
762       extends AbstractModelInstanceReadHandler<ITEM> {
763 
764     protected ModelInstanceReadHandler(
765         @NonNull IBoundInstanceModel<ITEM> instance,
766         @NonNull IBoundObject parentItem) {
767       super(instance, parentItem);
768     }
769 
770     @Override
771     public List<ITEM> readList() throws IOException {
772       @SuppressWarnings("PMD.CloseResource")
773       JsonParser parser = getReader();
774       URI resource = getSource();
775 
776       List<ITEM> items = new LinkedList<>();
777       switch (parser.currentToken()) {
778       case START_ARRAY:
779         // this is an array, we need to parse the array wrapper then each item
780         JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_ARRAY);
781 
782         // parse items
783         while (!JsonToken.END_ARRAY.equals(parser.currentToken())) {
784           items.add(readItem());
785         }
786 
787         // this is the other side of the array wrapper, advance past it
788         JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_ARRAY);
789         break;
790       case VALUE_NULL:
791         JsonUtil.assertAndAdvance(parser, resource, JsonToken.VALUE_NULL);
792         break;
793       default:
794         // this is a singleton, just parse the value as a single item
795         items.add(readItem());
796         break;
797       }
798       return items;
799     }
800 
801     @Override
802     public Map<String, ITEM> readMap() throws IOException {
803       @SuppressWarnings("PMD.CloseResource")
804       JsonParser parser = getReader();
805       URI resource = getSource();
806 
807       IBoundInstanceModel<?> instance = getCollectionInfo().getInstance();
808 
809       @SuppressWarnings("PMD.UseConcurrentHashMap")
810       Map<String, ITEM> items = new LinkedHashMap<>();
811 
812       // A map value is always wrapped in a START_OBJECT, since fields are used for
813       // the keys
814       JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_OBJECT);
815 
816       // process all map items
817       while (!JsonToken.END_OBJECT.equals(parser.currentToken())) {
818 
819         // a map item will always start with a FIELD_NAME, since this represents the key
820         JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME);
821 
822         // get the object, since it must have a JSON key
823         ITEM item = readItem();
824         if (item == null) {
825           throw new IOException(String.format("Null object encountered'%s.",
826               JsonUtil.generateLocationMessage(parser, resource)));
827         }
828 
829         // lookup the key
830         IBoundInstanceFlag jsonKey = instance.getItemJsonKey(item);
831         assert jsonKey != null;
832 
833         Object keyValue = jsonKey.getValue(item);
834         if (keyValue == null) {
835           throw new IOException(String.format("Null value for json-key for definition '%s'",
836               jsonKey.getContainingDefinition().toCoordinates()));
837         }
838         String key;
839         try {
840           key = jsonKey.getJavaTypeAdapter().asString(keyValue);
841         } catch (IllegalArgumentException ex) {
842           throw new IOException(
843               String.format("Malformed data '%s'%s. %s",
844                   keyValue,
845                   JsonUtil.generateLocationMessage(parser, resource),
846                   ex.getLocalizedMessage()),
847               ex);
848         }
849         items.put(key, item);
850 
851         // the next item will be a FIELD_NAME, or we will encounter an END_OBJECT if all
852         // items have been
853         // read
854         JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME, JsonToken.END_OBJECT);
855       }
856 
857       // A map value will always end with an end object, which needs to be consumed
858       JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_OBJECT);
859 
860       return items;
861     }
862 
863     @Override
864     public ITEM readItem() throws IOException {
865       IBoundInstanceModel<ITEM> instance = getCollectionInfo().getInstance();
866       return instance.readItem(getParentObject(), MetaschemaJsonReader.this);
867     }
868   }
869 
870   @SuppressWarnings("PMD.DataClass")
871   private static class MetaschemaData implements IMetaschemaData {
872     private final int line;
873     private final int column;
874     private final long charOffset;
875     private final long byteOffset;
876 
877     public MetaschemaData(@NonNull JsonLocation location) {
878       this.line = location.getLineNr();
879       this.column = location.getColumnNr();
880       this.charOffset = location.getCharOffset();
881       this.byteOffset = location.getByteOffset();
882     }
883 
884     @Override
885     public int getLine() {
886       return line;
887     }
888 
889     @Override
890     public int getColumn() {
891       return column;
892     }
893 
894     @Override
895     public long getCharOffset() {
896       return charOffset;
897     }
898 
899     @Override
900     public long getByteOffset() {
901       return byteOffset;
902     }
903   }
904 
905   @FunctionalInterface
906   private interface DefinitionBodyHandler<DEF extends IBoundDefinitionModelComplex> {
907     void accept(
908         @NonNull DEF definition,
909         @NonNull IBoundObject parent,
910         @NonNull IJsonProblemHandler problemHandler) throws IOException;
911   }
912 }