001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.databind.io.json;
028
029import com.fasterxml.jackson.core.JsonParser;
030import com.fasterxml.jackson.core.JsonToken;
031
032import gov.nist.secauto.metaschema.core.model.util.JsonUtil;
033import gov.nist.secauto.metaschema.core.util.ObjectUtils;
034import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
035import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance;
036import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance;
037import gov.nist.secauto.metaschema.databind.model.IBoundNamedInstance;
038import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance;
039import gov.nist.secauto.metaschema.databind.model.IClassBinding;
040import gov.nist.secauto.metaschema.databind.model.IFieldClassBinding;
041import gov.nist.secauto.metaschema.databind.model.info.IDataTypeHandler;
042import gov.nist.secauto.metaschema.databind.model.info.IModelPropertyInfo;
043import gov.nist.secauto.metaschema.databind.model.info.IPropertyCollector;
044
045import org.apache.logging.log4j.LogManager;
046import org.apache.logging.log4j.Logger;
047
048import java.io.IOException;
049import java.util.HashMap;
050import java.util.Map;
051
052import edu.umd.cs.findbugs.annotations.NonNull;
053import edu.umd.cs.findbugs.annotations.Nullable;
054
055public class MetaschemaJsonReader
056    implements IJsonParsingContext {
057  private static final Logger LOGGER = LogManager.getLogger(MetaschemaJsonReader.class);
058
059  @NonNull
060  private final JsonParser parser;
061  @NonNull
062  private final IJsonProblemHandler problemHandler;
063
064  /**
065   * Construct a new Module-aware JSON parser using the default problem handler.
066   *
067   * @param parser
068   *          the JSON parser to parse with
069   * @see DefaultJsonProblemHandler
070   */
071  public MetaschemaJsonReader(
072      @NonNull JsonParser parser) {
073    this(parser, new DefaultJsonProblemHandler());
074  }
075
076  /**
077   * Construct a new Module-aware JSON parser.
078   *
079   * @param parser
080   *          the JSON parser to parse with
081   * @param problemHandler
082   *          the problem handler implementation to use
083   */
084  public MetaschemaJsonReader(
085      @NonNull JsonParser parser,
086      @NonNull IJsonProblemHandler problemHandler) {
087    this.parser = parser;
088    this.problemHandler = problemHandler;
089  }
090
091  @Override
092  public JsonParser getReader() {
093    return parser;
094  }
095
096  @Override
097  public IJsonProblemHandler getProblemHandler() {
098    return problemHandler;
099  }
100
101  /**
102   * Parses JSON into a bound object. This assembly must be a root assembly for
103   * which a call to {@link IAssemblyClassBinding#isRoot()} will return
104   * {@code true}.
105   * <p>
106   * This method expects the parser's current token to be:
107   * <ul>
108   * <li>{@code null} indicating that the parser has not yet parsed a JSON
109   * node;</li>
110   * <li>a {@link JsonToken#START_OBJECT} which represents the object wrapper
111   * containing the root field,</li>
112   * <li>a {@link JsonToken#FIELD_NAME} representing the root field to parse,
113   * or</li>
114   * <li>a peer field to the root field that will be handled by the
115   * {@link IJsonProblemHandler#handleUnknownProperty(IClassBinding, Object, String, IJsonParsingContext)}
116   * method.</li>
117   * </ul>
118   * <p>
119   * After parsing the current token will be:
120   * <ul>
121   * <li>the next token after the {@link JsonToken#END_OBJECT} corresponding to
122   * the initial {@link JsonToken#START_OBJECT} parsed by this method;</li>
123   * <li>the next token after the {@link JsonToken#END_OBJECT} for the root
124   * field's value; or</li>
125   * <li>the next token after all fields and associated values have been parsed
126   * looking for the root field. This next token will be the
127   * {@link JsonToken#END_OBJECT} for the object containing the fields. In this
128   * case the method will throw an {@link IOException} indicating the root was not
129   * found.</li>
130   * </ul>
131   *
132   * @param <T>
133   *          the Java type of the resulting bound instance
134   * @param targetDefinition
135   *          the definition describing the root element data to parse
136   * @return the bound object instance representing the JSON object
137   * @throws IOException
138   *           if an error occurred while parsing the JSON
139   */
140  @SuppressWarnings({
141      "PMD.CyclomaticComplexity", "PMD.NPathComplexity" // acceptable
142  })
143  @Nullable
144  public <T> T read(@NonNull IAssemblyClassBinding targetDefinition) throws IOException {
145    if (!targetDefinition.isRoot()) {
146      throw new UnsupportedOperationException(
147          String.format("The assembly '%s' is not a root assembly.", targetDefinition.getBoundClass().getName()));
148    }
149
150    boolean objectWrapper = false;
151    if (parser.currentToken() == null) {
152      parser.nextToken();
153    }
154
155    if (JsonToken.START_OBJECT.equals(parser.currentToken())) {
156      // advance past the start object to the field name
157      JsonUtil.assertAndAdvance(parser, JsonToken.START_OBJECT);
158      objectWrapper = true;
159    }
160
161    String rootFieldName = targetDefinition.getRootJsonName();
162    JsonToken token;
163    Object instance = null;
164    while (!(JsonToken.END_OBJECT.equals(token = parser.currentToken()) || token == null)) {
165      if (!JsonToken.FIELD_NAME.equals(token)) {
166        throw new IOException(String.format("Expected FIELD_NAME token, found '%s'", token.toString()));
167      }
168
169      String fieldName = parser.currentName();
170      if (fieldName.equals(rootFieldName)) {
171        // process the object value, bound to the requested class
172        JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
173
174        // Make a temporary data type handler for the top-level definition
175        IDataTypeHandler dataTypeHandler = IDataTypeHandler.newDataTypeHandler(targetDefinition);
176
177        // read the top-level definition
178        instance = dataTypeHandler.readItem(null, this);
179
180        // stop now, since we found the root field
181        break;
182      }
183
184      if (!getProblemHandler().handleUnknownProperty(targetDefinition, instance, fieldName, this)) {
185        LOGGER.warn("Skipping unhandled top-level JSON field '{}'.", fieldName);
186        JsonUtil.skipNextValue(parser);
187      }
188    }
189
190    if (instance == null) {
191      throw new IOException(String.format("Failed to find root field '%s'.", rootFieldName));
192    }
193
194    if (objectWrapper) {
195      // advance past the end object
196      JsonUtil.assertAndAdvance(parser, JsonToken.END_OBJECT);
197    }
198
199    return ObjectUtils.asType(instance);
200  }
201
202  /**
203   * Read the data associated with the {@code instance} and apply it to the
204   * provided {@code parentObject}.
205   * <p>
206   * Consumes the field if the field's name matches. If it matches, then
207   * {@code true} is returned after parsing the value. Otherwise, {@code false} is
208   * returned to indicate the property was not parsed.
209   *
210   * @param targetInstance
211   *          the instance to parse data for
212   * @param parentObject
213   *          the Java object that data parsed by this method will be stored in
214   * @return {@code true} if the instance was parsed, or {@code false} if the data
215   *         did not contain information for this instance
216   * @throws IOException
217   *           if an error occurred while parsing the data
218   */
219  protected boolean readInstance(
220      @NonNull IBoundNamedInstance targetInstance,
221      @NonNull Object parentObject) throws IOException {
222    // the parser's current token should be the JSON field name
223    JsonUtil.assertCurrent(parser, JsonToken.FIELD_NAME);
224
225    String propertyName = parser.currentName();
226    if (LOGGER.isTraceEnabled()) {
227      LOGGER.trace("reading property {}", propertyName);
228    }
229
230    boolean handled = targetInstance.getJsonName().equals(propertyName);
231    if (handled) {
232      // advance past the field name
233      parser.nextToken();
234
235      Object value = readInstanceValue(targetInstance, parentObject);
236
237      if (value != null) {
238        targetInstance.setValue(parentObject, value);
239      }
240    }
241
242    // the current token will be either the next instance field name or the end of
243    // the parent object
244    JsonUtil.assertCurrent(parser, JsonToken.FIELD_NAME, JsonToken.END_OBJECT);
245    return handled;
246  }
247
248  /**
249   * Read the data associated with the {@code instance}.
250   *
251   * @param instance
252   *          the instance that describes the syntax of the data to read
253   * @param parentObject
254   *          the Java object that data parsed by this method will be stored in
255   * @return the parsed value(s)
256   * @throws IOException
257   *           if an error occurred while parsing the input
258   */
259  protected Object readInstanceValue(
260      @NonNull IBoundNamedInstance instance,
261      @NonNull Object parentObject) throws IOException {
262    Object value;
263    if (instance instanceof IBoundNamedModelInstance) {
264
265      // Deal with the collection or value type
266      IModelPropertyInfo info = ((IBoundNamedModelInstance) instance).getPropertyInfo();
267      IPropertyCollector collector = info.newPropertyCollector();
268
269      // let the property info parse the value
270      info.readValues(collector, parentObject, this);
271
272      // get the underlying value
273      value = collector.getValue();
274    } else if (instance instanceof IBoundFlagInstance) {
275      // just read the value directly
276      value = ((IBoundFlagInstance) instance).getDefinition().getJavaTypeAdapter().parse(parser);
277    } else if (instance instanceof IBoundFieldValueInstance) {
278      // just read the value directly
279      value = ((IBoundFieldValueInstance) instance).getJavaTypeAdapter().parse(parser);
280    } else {
281      throw new UnsupportedOperationException(
282          String.format("Unsupported instance type: %s", instance.getClass().getName()));
283    }
284    return value;
285  }
286
287  // @SuppressFBWarnings(value = "UC_USELESS_CONDITION", justification = "false
288  // positive")
289  @SuppressWarnings({
290      "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity" // acceptable
291  })
292  @Override
293  public void readDefinitionValue(
294      IClassBinding targetDefinition,
295      Object targetObject,
296      Map<String, ? extends IBoundNamedInstance> instances) throws IOException {
297    IBoundFlagInstance valueKeyFlag = null;
298    if (targetDefinition instanceof IFieldClassBinding) {
299      IFieldClassBinding targetFieldDefinition = (IFieldClassBinding) targetDefinition;
300      valueKeyFlag = targetFieldDefinition.getJsonValueKeyFlagInstance();
301    }
302
303    // make a copy, since we use the remaining values to initialize default values
304    Map<String, ? extends IBoundNamedInstance> remainingInstances = new HashMap<>(instances); // NOPMD not concurrent
305
306    // handle each property
307    while (!JsonToken.END_OBJECT.equals(parser.currentToken())) {
308      boolean handled = false;
309      String propertyName = parser.getCurrentName();
310      assert propertyName != null;
311
312      if (JsonToken.FIELD_NAME.equals(parser.currentToken())) {
313        // found a matching property
314        IBoundNamedInstance property = remainingInstances.get(propertyName);
315        if (property != null) {
316          handled = readInstance(property, targetObject);
317          remainingInstances.remove(propertyName);
318        }
319      } else {
320        throw new IOException(
321            String.format("Unexpected token: " + JsonUtil.toString(parser)));
322      }
323
324      if (!handled) {
325        if (valueKeyFlag != null) {
326          // Handle JSON value key flag case
327          IFieldClassBinding targetFieldDefinition = (IFieldClassBinding) targetDefinition;
328          valueKeyFlag.setValue(targetObject,
329              valueKeyFlag.getDefinition().getJavaTypeAdapter().parse(propertyName));
330
331          // advance past the FIELD_NAME to get the value
332          JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
333
334          IBoundFieldValueInstance fieldValue = targetFieldDefinition.getFieldValueInstance();
335          fieldValue.setValue(
336              targetObject,
337              fieldValue.getJavaTypeAdapter().parse(parser));
338          valueKeyFlag = null; // NOPMD used as boolean check to avoid value key check
339        } else if (!getProblemHandler().handleUnknownProperty(
340            targetDefinition,
341            targetObject,
342            propertyName,
343            this)) {
344          // handle unrecognized property case
345          if (LOGGER.isWarnEnabled()) {
346            LOGGER.warn("Unrecognized property named '{}' at '{}'", propertyName,
347                JsonUtil.toString(ObjectUtils.notNull(parser.getCurrentLocation())));
348          }
349          JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
350          JsonUtil.skipNextValue(parser);
351        }
352      }
353    }
354
355    if (!remainingInstances.isEmpty()) {
356      getProblemHandler().handleMissingInstances(
357          targetDefinition,
358          targetObject,
359          ObjectUtils.notNull(remainingInstances.values()));
360    }
361  }
362}