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