MetaschemaJsonReader.java
/*
* Portions of this software was developed by employees of the National Institute
* of Standards and Technology (NIST), an agency of the Federal Government and is
* being made available as a public service. Pursuant to title 17 United States
* Code Section 105, works of NIST employees are not subject to copyright
* protection in the United States. This software may be subject to foreign
* copyright. Permission in the United States and in foreign countries, to the
* extent that NIST may hold copyright, to use, copy, modify, create derivative
* works, and distribute this software and its documentation without fee is hereby
* granted on a non-exclusive basis, provided that this notice and disclaimer
* of warranty appears in all copies.
*
* THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
* EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
* THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
* INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
* SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT
* SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
* INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
* OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
* CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
* PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
* OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
*/
package gov.nist.secauto.metaschema.databind.io.json;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import gov.nist.secauto.metaschema.core.model.util.JsonUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance;
import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance;
import gov.nist.secauto.metaschema.databind.model.IBoundNamedInstance;
import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance;
import gov.nist.secauto.metaschema.databind.model.IClassBinding;
import gov.nist.secauto.metaschema.databind.model.IFieldClassBinding;
import gov.nist.secauto.metaschema.databind.model.info.IDataTypeHandler;
import gov.nist.secauto.metaschema.databind.model.info.IModelPropertyInfo;
import gov.nist.secauto.metaschema.databind.model.info.IPropertyCollector;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
public class MetaschemaJsonReader
implements IJsonParsingContext {
private static final Logger LOGGER = LogManager.getLogger(MetaschemaJsonReader.class);
@NonNull
private final JsonParser parser;
@NonNull
private final IJsonProblemHandler problemHandler;
/**
* Construct a new Module-aware JSON parser using the default problem handler.
*
* @param parser
* the JSON parser to parse with
* @see DefaultJsonProblemHandler
*/
public MetaschemaJsonReader(
@NonNull JsonParser parser) {
this(parser, new DefaultJsonProblemHandler());
}
/**
* Construct a new Module-aware JSON parser.
*
* @param parser
* the JSON parser to parse with
* @param problemHandler
* the problem handler implementation to use
*/
public MetaschemaJsonReader(
@NonNull JsonParser parser,
@NonNull IJsonProblemHandler problemHandler) {
this.parser = parser;
this.problemHandler = problemHandler;
}
@Override
public JsonParser getReader() {
return parser;
}
@Override
public IJsonProblemHandler getProblemHandler() {
return problemHandler;
}
/**
* Parses JSON into a bound object. This assembly must be a root assembly for
* which a call to {@link IAssemblyClassBinding#isRoot()} will return
* {@code true}.
* <p>
* This method expects the parser's current token to be:
* <ul>
* <li>{@code null} indicating that the parser has not yet parsed a JSON
* node;</li>
* <li>a {@link JsonToken#START_OBJECT} which represents the object wrapper
* containing the root field,</li>
* <li>a {@link JsonToken#FIELD_NAME} representing the root field to parse,
* or</li>
* <li>a peer field to the root field that will be handled by the
* {@link IJsonProblemHandler#handleUnknownProperty(IClassBinding, Object, String, IJsonParsingContext)}
* method.</li>
* </ul>
* <p>
* After parsing the current token will be:
* <ul>
* <li>the next token after the {@link JsonToken#END_OBJECT} corresponding to
* the initial {@link JsonToken#START_OBJECT} parsed by this method;</li>
* <li>the next token after the {@link JsonToken#END_OBJECT} for the root
* field's value; or</li>
* <li>the next token after all fields and associated values have been parsed
* looking for the root field. This next token will be the
* {@link JsonToken#END_OBJECT} for the object containing the fields. In this
* case the method will throw an {@link IOException} indicating the root was not
* found.</li>
* </ul>
*
* @param <T>
* the Java type of the resulting bound instance
* @param targetDefinition
* the definition describing the root element data to parse
* @return the bound object instance representing the JSON object
* @throws IOException
* if an error occurred while parsing the JSON
*/
@SuppressWarnings({
"PMD.CyclomaticComplexity", "PMD.NPathComplexity" // acceptable
})
@Nullable
public <T> T read(@NonNull IAssemblyClassBinding targetDefinition) throws IOException {
if (!targetDefinition.isRoot()) {
throw new UnsupportedOperationException(
String.format("The assembly '%s' is not a root assembly.", targetDefinition.getBoundClass().getName()));
}
boolean objectWrapper = false;
if (parser.currentToken() == null) {
parser.nextToken();
}
if (JsonToken.START_OBJECT.equals(parser.currentToken())) {
// advance past the start object to the field name
JsonUtil.assertAndAdvance(parser, JsonToken.START_OBJECT);
objectWrapper = true;
}
String rootFieldName = targetDefinition.getRootJsonName();
JsonToken token;
Object instance = null;
while (!(JsonToken.END_OBJECT.equals(token = parser.currentToken()) || token == null)) {
if (!JsonToken.FIELD_NAME.equals(token)) {
throw new IOException(String.format("Expected FIELD_NAME token, found '%s'", token.toString()));
}
String fieldName = parser.currentName();
if (fieldName.equals(rootFieldName)) {
// process the object value, bound to the requested class
JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
// Make a temporary data type handler for the top-level definition
IDataTypeHandler dataTypeHandler = IDataTypeHandler.newDataTypeHandler(targetDefinition);
// read the top-level definition
instance = dataTypeHandler.readItem(null, this);
// stop now, since we found the root field
break;
}
if (!getProblemHandler().handleUnknownProperty(targetDefinition, instance, fieldName, this)) {
LOGGER.warn("Skipping unhandled top-level JSON field '{}'.", fieldName);
JsonUtil.skipNextValue(parser);
}
}
if (instance == null) {
throw new IOException(String.format("Failed to find root field '%s'.", rootFieldName));
}
if (objectWrapper) {
// advance past the end object
JsonUtil.assertAndAdvance(parser, JsonToken.END_OBJECT);
}
return ObjectUtils.asType(instance);
}
/**
* Read the data associated with the {@code instance} and apply it to the
* provided {@code parentObject}.
* <p>
* Consumes the field if the field's name matches. If it matches, then
* {@code true} is returned after parsing the value. Otherwise, {@code false} is
* returned to indicate the property was not parsed.
*
* @param targetInstance
* the instance to parse data for
* @param parentObject
* the Java object that data parsed by this method will be stored in
* @return {@code true} if the instance was parsed, or {@code false} if the data
* did not contain information for this instance
* @throws IOException
* if an error occurred while parsing the data
*/
protected boolean readInstance(
@NonNull IBoundNamedInstance targetInstance,
@NonNull Object parentObject) throws IOException {
// the parser's current token should be the JSON field name
JsonUtil.assertCurrent(parser, JsonToken.FIELD_NAME);
String propertyName = parser.currentName();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("reading property {}", propertyName);
}
boolean handled = targetInstance.getJsonName().equals(propertyName);
if (handled) {
// advance past the field name
parser.nextToken();
Object value = readInstanceValue(targetInstance, parentObject);
if (value != null) {
targetInstance.setValue(parentObject, value);
}
}
// the current token will be either the next instance field name or the end of
// the parent object
JsonUtil.assertCurrent(parser, JsonToken.FIELD_NAME, JsonToken.END_OBJECT);
return handled;
}
/**
* Read the data associated with the {@code instance}.
*
* @param instance
* the instance that describes the syntax of the data to read
* @param parentObject
* the Java object that data parsed by this method will be stored in
* @return the parsed value(s)
* @throws IOException
* if an error occurred while parsing the input
*/
protected Object readInstanceValue(
@NonNull IBoundNamedInstance instance,
@NonNull Object parentObject) throws IOException {
Object value;
if (instance instanceof IBoundNamedModelInstance) {
// Deal with the collection or value type
IModelPropertyInfo info = ((IBoundNamedModelInstance) instance).getPropertyInfo();
IPropertyCollector collector = info.newPropertyCollector();
// let the property info parse the value
info.readValues(collector, parentObject, this);
// get the underlying value
value = collector.getValue();
} else if (instance instanceof IBoundFlagInstance) {
// just read the value directly
value = ((IBoundFlagInstance) instance).getDefinition().getJavaTypeAdapter().parse(parser);
} else if (instance instanceof IBoundFieldValueInstance) {
// just read the value directly
value = ((IBoundFieldValueInstance) instance).getJavaTypeAdapter().parse(parser);
} else {
throw new UnsupportedOperationException(
String.format("Unsupported instance type: %s", instance.getClass().getName()));
}
return value;
}
// @SuppressFBWarnings(value = "UC_USELESS_CONDITION", justification = "false
// positive")
@SuppressWarnings({
"PMD.CyclomaticComplexity", "PMD.CognitiveComplexity" // acceptable
})
@Override
public void readDefinitionValue(
IClassBinding targetDefinition,
Object targetObject,
Map<String, ? extends IBoundNamedInstance> instances) throws IOException {
IBoundFlagInstance valueKeyFlag = null;
if (targetDefinition instanceof IFieldClassBinding) {
IFieldClassBinding targetFieldDefinition = (IFieldClassBinding) targetDefinition;
valueKeyFlag = targetFieldDefinition.getJsonValueKeyFlagInstance();
}
// make a copy, since we use the remaining values to initialize default values
Map<String, ? extends IBoundNamedInstance> remainingInstances = new HashMap<>(instances); // NOPMD not concurrent
// handle each property
while (!JsonToken.END_OBJECT.equals(parser.currentToken())) {
boolean handled = false;
String propertyName = parser.getCurrentName();
assert propertyName != null;
if (JsonToken.FIELD_NAME.equals(parser.currentToken())) {
// found a matching property
IBoundNamedInstance property = remainingInstances.get(propertyName);
if (property != null) {
handled = readInstance(property, targetObject);
remainingInstances.remove(propertyName);
}
} else {
throw new IOException(
String.format("Unexpected token: " + JsonUtil.toString(parser)));
}
if (!handled) {
if (valueKeyFlag != null) {
// Handle JSON value key flag case
IFieldClassBinding targetFieldDefinition = (IFieldClassBinding) targetDefinition;
valueKeyFlag.setValue(targetObject,
valueKeyFlag.getDefinition().getJavaTypeAdapter().parse(propertyName));
// advance past the FIELD_NAME to get the value
JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
IBoundFieldValueInstance fieldValue = targetFieldDefinition.getFieldValueInstance();
fieldValue.setValue(
targetObject,
fieldValue.getJavaTypeAdapter().parse(parser));
valueKeyFlag = null; // NOPMD used as boolean check to avoid value key check
} else if (!getProblemHandler().handleUnknownProperty(
targetDefinition,
targetObject,
propertyName,
this)) {
// handle unrecognized property case
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Unrecognized property named '{}' at '{}'", propertyName,
JsonUtil.toString(ObjectUtils.notNull(parser.getCurrentLocation())));
}
JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
JsonUtil.skipNextValue(parser);
}
}
}
if (!remainingInstances.isEmpty()) {
getProblemHandler().handleMissingInstances(
targetDefinition,
targetObject,
ObjectUtils.notNull(remainingInstances.values()));
}
}
}