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