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