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