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) throws IOException {
369    if (value == null && LOGGER.isWarnEnabled()) {
370      LOGGER.atWarn().log("Missing property value{}",
371          JsonUtil.generateLocationMessage(getReader(), getSource()));
372    }
373    // TODO: change nullness annotations to be @Nullable
374    return value;
375  }
376
377  @Override
378  public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundInstanceModelAssembly instance)
379      throws IOException {
380    IBoundInstanceFlag jsonKey = instance.getJsonKey();
381    IBoundDefinitionModelComplex definition = instance.getDefinition();
382    return readComplexDefinitionObject(
383        parentItem,
384        definition,
385        jsonKey,
386        new PropertyBodyHandler(instance.getJsonProperties()),
387        getProblemHandler());
388  }
389
390  @Override
391  public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundInstanceModelGroupedAssembly instance)
392      throws IOException {
393    return readComplexDefinitionObject(
394        parentItem,
395        instance.getDefinition(),
396        instance.getEffectiveJsonKey(),
397        new PropertyBodyHandler(instance.getJsonProperties()),
398        new GroupedInstanceProblemHandler(instance, getProblemHandler()));
399  }
400
401  @Override
402  public IBoundObject readItemAssembly(IBoundObject parentItem, IBoundDefinitionModelAssembly definition)
403      throws IOException {
404    return readComplexDefinitionObject(
405        parentItem,
406        definition,
407        null,
408        new PropertyBodyHandler(definition.getJsonProperties()),
409        getProblemHandler());
410  }
411
412  @NonNull
413  private Object readScalarItem(@NonNull IFeatureScalarItemValueHandler handler)
414      throws IOException {
415    return handler.getJavaTypeAdapter().parse(getReader(), getSource());
416  }
417
418  @NonNull
419  private IBoundObject readFieldObject(
420      @Nullable IBoundObject parentItem,
421      @NonNull IBoundDefinitionModelFieldComplex definition,
422      @NonNull Map<String, IBoundProperty<?>> jsonProperties,
423      @Nullable IBoundInstanceFlag jsonKey,
424      @NonNull IJsonProblemHandler problemHandler) throws IOException {
425    IBoundInstanceFlag jsonValueKey = definition.getJsonValueKeyFlagInstance();
426    IJsonProblemHandler actualProblemHandler = jsonValueKey == null
427        ? problemHandler
428        : new JsomValueKeyProblemHandler(problemHandler, jsonValueKey);
429
430    IBoundObject retval;
431    if (jsonProperties.isEmpty() && jsonValueKey == null) {
432      retval = readComplexDefinitionObject(
433          parentItem,
434          definition,
435          jsonKey,
436          (def, parent, problem) -> {
437            IBoundFieldValue fieldValue = definition.getFieldValue();
438            Object item = readItemFieldValue(parent, fieldValue);
439            if (item != null) {
440              fieldValue.setValue(parent, item);
441            }
442          },
443          actualProblemHandler);
444
445    } else {
446      retval = readComplexDefinitionObject(
447          parentItem,
448          definition,
449          jsonKey,
450          new PropertyBodyHandler(jsonProperties),
451          actualProblemHandler);
452    }
453    return retval;
454  }
455
456  @NonNull
457  private IBoundObject readComplexDefinitionObject(
458      @Nullable IBoundObject parentItem,
459      @NonNull IBoundDefinitionModelComplex definition,
460      @Nullable IBoundInstanceFlag jsonKey,
461      @NonNull DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler,
462      @NonNull IJsonProblemHandler problemHandler) throws IOException {
463    DefinitionBodyHandler<IBoundDefinitionModelComplex> actualBodyHandler = jsonKey == null
464        ? bodyHandler
465        : new JsonKeyBodyHandler(jsonKey, bodyHandler);
466
467    @SuppressWarnings("PMD.CloseResource")
468    JsonLocation location = getReader().currentLocation();
469
470    // construct the item
471    IBoundObject item = definition.newInstance(
472        JsonLocation.NA.equals(location)
473            ? null
474            : () -> new MetaschemaData(ObjectUtils.requireNonNull(location)));
475
476    try {
477      // call pre-parse initialization hook
478      definition.callBeforeDeserialize(item, parentItem);
479
480      // read the property values
481      actualBodyHandler.accept(definition, item, problemHandler);
482
483      // call post-parse initialization hook
484      definition.callAfterDeserialize(item, parentItem);
485    } catch (BindingException ex) {
486      throw new IOException(ex);
487    }
488
489    return item;
490  }
491
492  @SuppressWarnings("resource")
493  @Override
494  public IBoundObject readChoiceGroupItem(IBoundObject parentItem, IBoundInstanceModelChoiceGroup instance)
495      throws IOException {
496    @SuppressWarnings("PMD.CloseResource")
497    JsonParser parser = getReader();
498    ObjectNode node = parser.readValueAsTree();
499
500    String discriminatorProperty = instance.getJsonDiscriminatorProperty();
501    JsonNode discriminatorNode = node.get(discriminatorProperty);
502    if (discriminatorNode == null) {
503      throw new IllegalArgumentException(String.format(
504          "Unable to find discriminator property '%s' for object at '%s'.",
505          discriminatorProperty,
506          JsonUtil.toString(parser, getSource())));
507    }
508    String discriminator = ObjectUtils.requireNonNull(discriminatorNode.asText());
509
510    IBoundInstanceModelGroupedNamed actualInstance = instance.getGroupedModelInstance(discriminator);
511    assert actualInstance != null;
512
513    IBoundObject retval;
514    try (JsonParser newParser = node.traverse(parser.getCodec())) {
515      assert newParser != null;
516      push(newParser);
517
518      // get initial token
519      retval = actualInstance.readItem(parentItem, this);
520      assert newParser.currentToken() == null;
521      pop(newParser);
522    }
523
524    // advance the original parser to the next token
525    parser.nextToken();
526
527    return retval;
528  }
529
530  private final class JsonKeyBodyHandler implements DefinitionBodyHandler<IBoundDefinitionModelComplex> {
531    @NonNull
532    private final IBoundInstanceFlag jsonKey;
533    @NonNull
534    private final DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler;
535
536    private JsonKeyBodyHandler(
537        @NonNull IBoundInstanceFlag jsonKey,
538        @NonNull DefinitionBodyHandler<IBoundDefinitionModelComplex> bodyHandler) {
539      this.jsonKey = jsonKey;
540      this.bodyHandler = bodyHandler;
541    }
542
543    @Override
544    public void accept(
545        IBoundDefinitionModelComplex definition,
546        IBoundObject parent,
547        IJsonProblemHandler problemHandler)
548        throws IOException {
549      @SuppressWarnings("PMD.CloseResource")
550      JsonParser parser = getReader();
551      URI resource = getSource();
552      JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME);
553
554      // the field will be the JSON key
555      String key = ObjectUtils.notNull(parser.currentName());
556      try {
557        Object value = jsonKey.getDefinition().getJavaTypeAdapter().parse(key);
558        jsonKey.setValue(parent, ObjectUtils.notNull(value.toString()));
559      } catch (IllegalArgumentException ex) {
560        throw new IOException(
561            String.format("Malformed data '%s'%s. %s",
562                key,
563                JsonUtil.generateLocationMessage(parser, resource),
564                ex.getLocalizedMessage()),
565            ex);
566      }
567
568      // skip to the next token
569      parser.nextToken();
570      // JsonUtil.assertCurrent(parser, JsonToken.START_OBJECT);
571
572      // // advance past the JSON key's start object
573      // JsonUtil.assertAndAdvance(parser, JsonToken.START_OBJECT);
574
575      // read the property values
576      bodyHandler.accept(definition, parent, problemHandler);
577
578      // // advance past the JSON key's end object
579      // JsonUtil.assertAndAdvance(parser, JsonToken.END_OBJECT);
580    }
581  }
582
583  private final class PropertyBodyHandler implements DefinitionBodyHandler<IBoundDefinitionModelComplex> {
584    @NonNull
585    private final Map<String, IBoundProperty<?>> jsonProperties;
586
587    private PropertyBodyHandler(@NonNull Map<String, IBoundProperty<?>> jsonProperties) {
588      this.jsonProperties = jsonProperties;
589    }
590
591    @Override
592    public void accept(
593        IBoundDefinitionModelComplex definition,
594        IBoundObject parent,
595        IJsonProblemHandler problemHandler)
596        throws IOException {
597      @SuppressWarnings("PMD.CloseResource")
598      JsonParser parser = getReader();
599      URI resource = getSource();
600
601      // advance past the start object
602      JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_OBJECT);
603
604      // make a copy, since we use the remaining values to initialize default values
605      Map<String, IBoundProperty<?>> remainingInstances = new HashMap<>(jsonProperties); // NOPMD not concurrent
606
607      // handle each property
608      while (JsonToken.FIELD_NAME.equals(parser.currentToken())) {
609
610        // the parser's current token should be the JSON field name
611        String propertyName = ObjectUtils.notNull(parser.currentName());
612        if (LOGGER.isTraceEnabled()) {
613          LOGGER.trace("reading property {}", propertyName);
614        }
615
616        IBoundProperty<?> property = remainingInstances.get(propertyName);
617
618        boolean handled = false;
619        if (property != null) {
620          // advance past the field name
621          parser.nextToken();
622
623          Object value = readObjectProperty(parent, property);
624          if (value != null) {
625            property.setValue(parent, value);
626          }
627
628          // mark handled
629          remainingInstances.remove(propertyName);
630          handled = true;
631        }
632
633        if (!handled && !problemHandler.handleUnknownProperty(
634            definition,
635            parent,
636            propertyName,
637            MetaschemaJsonReader.this)) {
638          if (LOGGER.isWarnEnabled()) {
639            LOGGER.warn("Skipping unhandled JSON field '{}' {}.", propertyName, JsonUtil.toString(parser, resource));
640          }
641          JsonUtil.assertAndAdvance(parser, resource, JsonToken.FIELD_NAME);
642          JsonUtil.skipNextValue(parser, resource);
643        }
644
645        // the current token will be either the next instance field name or the end of
646        // the parent object
647        JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME, JsonToken.END_OBJECT);
648      }
649
650      problemHandler.handleMissingInstances(
651          definition,
652          parent,
653          ObjectUtils.notNull(remainingInstances.values()));
654
655      // advance past the end object
656      JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_OBJECT);
657    }
658  }
659
660  private static final class GroupedInstanceProblemHandler implements IJsonProblemHandler {
661    @NonNull
662    private final IBoundInstanceModelGroupedNamed instance;
663    @NonNull
664    private final IJsonProblemHandler delegate;
665
666    private GroupedInstanceProblemHandler(
667        @NonNull IBoundInstanceModelGroupedNamed instance,
668        @NonNull IJsonProblemHandler delegate) {
669      this.instance = instance;
670      this.delegate = delegate;
671    }
672
673    @Override
674    public void handleMissingInstances(
675        IBoundDefinitionModelComplex parentDefinition,
676        IBoundObject targetObject,
677        Collection<? extends IBoundProperty<?>> unhandledInstances) throws IOException {
678      delegate.handleMissingInstances(parentDefinition, targetObject, unhandledInstances);
679    }
680
681    @Override
682    public boolean handleUnknownProperty(
683        IBoundDefinitionModelComplex definition,
684        IBoundObject parentItem,
685        String fieldName,
686        IJsonParsingContext parsingContext) throws IOException {
687      boolean retval;
688      if (instance.getParentContainer().getJsonDiscriminatorProperty().equals(fieldName)) {
689        JsonUtil.skipNextValue(parsingContext.getReader(), parsingContext.getSource());
690        retval = true;
691      } else {
692        retval = delegate.handleUnknownProperty(definition, parentItem, fieldName, parsingContext);
693      }
694      return retval;
695    }
696  }
697
698  private final class JsomValueKeyProblemHandler implements IJsonProblemHandler {
699    @NonNull
700    private final IJsonProblemHandler delegate;
701    @NonNull
702    private final IBoundInstanceFlag jsonValueKeyFlag;
703    private boolean foundJsonValueKey; // false
704
705    private JsomValueKeyProblemHandler(
706        @NonNull IJsonProblemHandler delegate,
707        @NonNull IBoundInstanceFlag jsonValueKeyFlag) {
708      this.delegate = delegate;
709      this.jsonValueKeyFlag = jsonValueKeyFlag;
710    }
711
712    @Override
713    public void handleMissingInstances(
714        IBoundDefinitionModelComplex parentDefinition,
715        IBoundObject targetObject,
716        Collection<? extends IBoundProperty<?>> unhandledInstances) throws IOException {
717      delegate.handleMissingInstances(parentDefinition, targetObject, unhandledInstances);
718    }
719
720    @Override
721    public boolean handleUnknownProperty(
722        IBoundDefinitionModelComplex definition,
723        IBoundObject parentItem,
724        String fieldName,
725        IJsonParsingContext parsingContext) throws IOException {
726      boolean retval;
727      if (foundJsonValueKey) {
728        retval = delegate.handleUnknownProperty(definition, parentItem, fieldName, parsingContext);
729      } else {
730        @SuppressWarnings("PMD.CloseResource")
731        JsonParser parser = parsingContext.getReader();
732        URI resource = parsingContext.getSource();
733
734        // handle JSON value key
735        String key = ObjectUtils.notNull(parser.currentName());
736        try {
737          Object keyValue = jsonValueKeyFlag.getJavaTypeAdapter().parse(key);
738          jsonValueKeyFlag.setValue(ObjectUtils.notNull(parentItem), keyValue);
739        } catch (IllegalArgumentException ex) {
740          throw new IOException(
741              String.format("Malformed data '%s'%s. %s",
742                  key,
743                  JsonUtil.generateLocationMessage(parser, resource),
744                  ex.getLocalizedMessage()),
745              ex);
746        }
747        // advance past the field name
748        JsonUtil.assertAndAdvance(parser, resource, JsonToken.FIELD_NAME);
749
750        IBoundFieldValue fieldValue = ((IBoundDefinitionModelFieldComplex) definition).getFieldValue();
751        Object value = readItemFieldValue(ObjectUtils.notNull(parentItem), fieldValue);
752        if (value != null) {
753          fieldValue.setValue(ObjectUtils.notNull(parentItem), value);
754        }
755
756        retval = foundJsonValueKey = true;
757      }
758      return retval;
759    }
760  }
761
762  private class ModelInstanceReadHandler<ITEM>
763      extends AbstractModelInstanceReadHandler<ITEM> {
764
765    protected ModelInstanceReadHandler(
766        @NonNull IBoundInstanceModel<ITEM> instance,
767        @NonNull IBoundObject parentItem) {
768      super(instance, parentItem);
769    }
770
771    @Override
772    public List<ITEM> readList() throws IOException {
773      @SuppressWarnings("PMD.CloseResource")
774      JsonParser parser = getReader();
775      URI resource = getSource();
776
777      List<ITEM> items = new LinkedList<>();
778      switch (parser.currentToken()) {
779      case START_ARRAY:
780        // this is an array, we need to parse the array wrapper then each item
781        JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_ARRAY);
782
783        // parse items
784        while (!JsonToken.END_ARRAY.equals(parser.currentToken())) {
785          items.add(readItem());
786        }
787
788        // this is the other side of the array wrapper, advance past it
789        JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_ARRAY);
790        break;
791      case VALUE_NULL:
792        JsonUtil.assertAndAdvance(parser, resource, JsonToken.VALUE_NULL);
793        break;
794      default:
795        // this is a singleton, just parse the value as a single item
796        items.add(readItem());
797        break;
798      }
799      return items;
800    }
801
802    @Override
803    public Map<String, ITEM> readMap() throws IOException {
804      @SuppressWarnings("PMD.CloseResource")
805      JsonParser parser = getReader();
806      URI resource = getSource();
807
808      IBoundInstanceModel<?> instance = getCollectionInfo().getInstance();
809
810      @SuppressWarnings("PMD.UseConcurrentHashMap")
811      Map<String, ITEM> items = new LinkedHashMap<>();
812
813      // A map value is always wrapped in a START_OBJECT, since fields are used for
814      // the keys
815      JsonUtil.assertAndAdvance(parser, resource, JsonToken.START_OBJECT);
816
817      // process all map items
818      while (!JsonToken.END_OBJECT.equals(parser.currentToken())) {
819
820        // a map item will always start with a FIELD_NAME, since this represents the key
821        JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME);
822
823        // get the object, since it must have a JSON key
824        ITEM item = readItem();
825        if (item == null) {
826          throw new IOException(String.format("Null object encountered'%s.",
827              JsonUtil.generateLocationMessage(parser, resource)));
828        }
829
830        // lookup the key
831        IBoundInstanceFlag jsonKey = instance.getItemJsonKey(item);
832        assert jsonKey != null;
833
834        Object keyValue = jsonKey.getValue(item);
835        if (keyValue == null) {
836          throw new IOException(String.format("Null value for json-key for definition '%s'",
837              jsonKey.getContainingDefinition().toCoordinates()));
838        }
839        String key;
840        try {
841          key = jsonKey.getJavaTypeAdapter().asString(keyValue);
842        } catch (IllegalArgumentException ex) {
843          throw new IOException(
844              String.format("Malformed data '%s'%s. %s",
845                  keyValue,
846                  JsonUtil.generateLocationMessage(parser, resource),
847                  ex.getLocalizedMessage()),
848              ex);
849        }
850        items.put(key, item);
851
852        // the next item will be a FIELD_NAME, or we will encounter an END_OBJECT if all
853        // items have been
854        // read
855        JsonUtil.assertCurrent(parser, resource, JsonToken.FIELD_NAME, JsonToken.END_OBJECT);
856      }
857
858      // A map value will always end with an end object, which needs to be consumed
859      JsonUtil.assertAndAdvance(parser, resource, JsonToken.END_OBJECT);
860
861      return items;
862    }
863
864    @Override
865    public ITEM readItem() throws IOException {
866      IBoundInstanceModel<ITEM> instance = getCollectionInfo().getInstance();
867      return instance.readItem(getParentObject(), MetaschemaJsonReader.this);
868    }
869  }
870
871  @SuppressWarnings("PMD.DataClass")
872  private static class MetaschemaData implements IMetaschemaData {
873    private final int line;
874    private final int column;
875    private final long charOffset;
876    private final long byteOffset;
877
878    public MetaschemaData(@NonNull JsonLocation location) {
879      this.line = location.getLineNr();
880      this.column = location.getColumnNr();
881      this.charOffset = location.getCharOffset();
882      this.byteOffset = location.getByteOffset();
883    }
884
885    @Override
886    public int getLine() {
887      return line;
888    }
889
890    @Override
891    public int getColumn() {
892      return column;
893    }
894
895    @Override
896    public long getCharOffset() {
897      return charOffset;
898    }
899
900    @Override
901    public long getByteOffset() {
902      return byteOffset;
903    }
904  }
905
906  @FunctionalInterface
907  private interface DefinitionBodyHandler<DEF extends IBoundDefinitionModelComplex> {
908    void accept(
909        @NonNull DEF definition,
910        @NonNull IBoundObject parent,
911        @NonNull IJsonProblemHandler problemHandler) throws IOException;
912  }
913}