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