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