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