MetaschemaJsonWriter.java
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/
package gov.nist.secauto.metaschema.databind.io.json;
import com.fasterxml.jackson.core.JsonGenerator;
import gov.nist.secauto.metaschema.core.model.IBoundObject;
import gov.nist.secauto.metaschema.core.model.JsonGroupAsBehavior;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
import gov.nist.secauto.metaschema.databind.model.IBoundInstance;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModel;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelAssembly;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldComplex;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldScalar;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
import gov.nist.secauto.metaschema.databind.model.IBoundProperty;
import gov.nist.secauto.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
import gov.nist.secauto.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
import gov.nist.secauto.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
import gov.nist.secauto.metaschema.databind.model.info.IItemWriteHandler;
import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo;
import java.io.IOException;
import java.util.List;
import edu.umd.cs.findbugs.annotations.NonNull;
@SuppressWarnings("PMD.CouplingBetweenObjects")
public class MetaschemaJsonWriter implements IJsonWritingContext, IItemWriteHandler {
@NonNull
private final JsonGenerator generator;
/**
* Construct a new Module-aware JSON writer.
*
* @param generator
* the JSON generator to write with
* @see DefaultJsonProblemHandler
*/
public MetaschemaJsonWriter(@NonNull JsonGenerator generator) {
this.generator = generator;
}
@Override
public JsonGenerator getWriter() {
return generator;
}
// =====================================
// Entry point for top-level-definitions
// =====================================
@Override
public void write(
@NonNull IBoundDefinitionModelComplex definition,
@NonNull IBoundObject item) throws IOException {
definition.writeItem(item, this);
}
// ================
// Instance writers
// ================
private <T> void writeInstance(
@NonNull IBoundProperty<T> instance,
@NonNull IBoundObject parentItem) throws IOException {
@SuppressWarnings("unchecked")
T value = (T) instance.getValue(parentItem);
if (value != null && !value.equals(instance.getResolvedDefaultValue())) {
generator.writeFieldName(instance.getJsonName());
instance.writeItem(value, this);
}
}
private <T> void writeModelInstance(
@NonNull IBoundInstanceModel<T> instance,
@NonNull Object parentItem) throws IOException {
Object value = instance.getValue(parentItem);
if (value != null) {
// this if is not strictly needed, since isEmpty will return false on a null
// value
// checking null here potentially avoids the expensive operation of instatiating
IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
if (!collectionInfo.isEmpty(value)) {
generator.writeFieldName(instance.getJsonName());
collectionInfo.writeItems(new ModelInstanceWriteHandler<>(instance), value);
}
}
}
@SuppressWarnings("PMD.NullAssignment")
private void writeFieldValue(@NonNull IBoundFieldValue fieldValue, @NonNull Object parentItem) throws IOException {
Object item = fieldValue.getValue(parentItem);
// handle json value key
IBoundInstanceFlag jsonValueKey = fieldValue.getParentFieldDefinition().getJsonValueKeyFlagInstance();
if (item == null) {
if (jsonValueKey != null) {
item = fieldValue.getDefaultValue();
}
} else if (item.equals(fieldValue.getResolvedDefaultValue())) {
// same as default
item = null;
}
if (item != null) {
// There are two modes:
// 1) use of a JSON value key, or
// 2) a simple value named "value"
String valueKeyName;
if (jsonValueKey != null) {
Object keyValue = jsonValueKey.getValue(parentItem);
if (keyValue == null) {
throw new IOException(String.format("Null value for json-value-key for definition '%s'",
jsonValueKey.getContainingDefinition().toCoordinates()));
}
try {
// this is the JSON value key case
valueKeyName = jsonValueKey.getJavaTypeAdapter().asString(keyValue);
} catch (IllegalArgumentException ex) {
throw new IOException(
String.format("Invalid value '%s' for json-value-key for definition '%s'",
keyValue,
jsonValueKey.getContainingDefinition().toCoordinates()),
ex);
}
} else {
valueKeyName = fieldValue.getParentFieldDefinition().getEffectiveJsonValueKeyName();
}
generator.writeFieldName(valueKeyName);
// LOGGER.info("FIELD: {}", valueKeyName);
writeItemFieldValue(item, fieldValue);
}
}
@Override
public void writeItemFlag(Object item, IBoundInstanceFlag instance) throws IOException {
writeScalarItem(item, instance);
}
@Override
public void writeItemField(Object item, IBoundInstanceModelFieldScalar instance) throws IOException {
writeScalarItem(item, instance);
}
@Override
public void writeItemField(IBoundObject item, IBoundInstanceModelFieldComplex instance) throws IOException {
writeModelObject(
instance,
item,
this::writeObjectProperties);
}
@Override
public void writeItemField(IBoundObject item, IBoundInstanceModelGroupedField instance) throws IOException {
writeGroupedModelObject(
instance,
item,
(parent, handler) -> {
writeDiscriminatorProperty(handler);
writeObjectProperties(parent, handler);
});
}
@Override
public void writeItemField(IBoundObject item, IBoundDefinitionModelFieldComplex definition) throws IOException {
writeDefinitionObject(
definition,
item,
(ObjectWriter<IBoundDefinitionModelFieldComplex>) this::writeObjectProperties);
}
@Override
public void writeItemFieldValue(Object item, IBoundFieldValue fieldValue) throws IOException {
fieldValue.getJavaTypeAdapter().writeJsonValue(item, generator);
}
@Override
public void writeItemAssembly(IBoundObject item, IBoundInstanceModelAssembly instance) throws IOException {
writeModelObject(instance, item, this::writeObjectProperties);
}
@Override
public void writeItemAssembly(IBoundObject item, IBoundInstanceModelGroupedAssembly instance) throws IOException {
writeGroupedModelObject(
instance,
item,
(parent, handler) -> {
writeDiscriminatorProperty(handler);
writeObjectProperties(parent, handler);
});
}
@Override
public void writeItemAssembly(IBoundObject item, IBoundDefinitionModelAssembly definition) throws IOException {
writeDefinitionObject(definition, item, this::writeObjectProperties);
}
@Override
public void writeChoiceGroupItem(IBoundObject item, IBoundInstanceModelChoiceGroup instance) throws IOException {
IBoundInstanceModelGroupedNamed actualInstance = instance.getItemInstance(item);
assert actualInstance != null;
actualInstance.writeItem(item, this);
}
/**
* Writes a scalar item.
*
* @param item
* the item to write
* @param handler
* the value handler
* @throws IOException
* if an error occurred while writing the scalar value
*/
private void writeScalarItem(@NonNull Object item, @NonNull IFeatureScalarItemValueHandler handler)
throws IOException {
handler.getJavaTypeAdapter().writeJsonValue(item, generator);
}
private <T extends IBoundInstanceModelGroupedNamed> void writeDiscriminatorProperty(
@NonNull T instance) throws IOException {
IBoundInstanceModelChoiceGroup choiceGroup = instance.getParentContainer();
// write JSON object discriminator
String discriminatorProperty = choiceGroup.getJsonDiscriminatorProperty();
String discriminatorValue = instance.getEffectiveDisciminatorValue();
generator.writeStringField(discriminatorProperty, discriminatorValue);
}
private <T extends IFeatureComplexItemValueHandler> void writeObjectProperties(
@NonNull IBoundObject parent,
@NonNull T handler) throws IOException {
for (IBoundProperty<?> property : handler.getJsonProperties().values()) {
assert property != null;
if (property instanceof IBoundInstanceModel) {
writeModelInstance((IBoundInstanceModel<?>) property, parent);
} else if (property instanceof IBoundInstance) {
writeInstance(property, parent);
} else { // IBoundFieldValue
writeFieldValue((IBoundFieldValue) property, parent);
}
}
}
private <T extends IFeatureComplexItemValueHandler> void writeDefinitionObject(
@NonNull T handler,
@NonNull IBoundObject parent,
@NonNull ObjectWriter<T> propertyWriter) throws IOException {
generator.writeStartObject();
propertyWriter.accept(parent, handler);
generator.writeEndObject();
}
private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModel<IBoundObject>>
void writeModelObject(
@NonNull T handler,
@NonNull IBoundObject parent,
@NonNull ObjectWriter<T> propertyWriter) throws IOException {
generator.writeStartObject();
IBoundInstanceFlag jsonKey = handler.getItemJsonKey(parent);
if (jsonKey != null) {
Object keyValue = jsonKey.getValue(parent);
if (keyValue == null) {
throw new IOException(
String.format("Null value for json-key for definition '%s'",
jsonKey.getContainingDefinition().toCoordinates()));
}
// the field will be the JSON key value
String key;
try {
key = jsonKey.getJavaTypeAdapter().asString(keyValue);
} catch (IllegalArgumentException ex) {
throw new IOException(
String.format("Illegal value '%s' for json-key for definition '%s'",
keyValue,
jsonKey.getContainingDefinition().toCoordinates()),
ex);
}
generator.writeFieldName(key);
// next the value will be a start object
generator.writeStartObject();
}
propertyWriter.accept(parent, handler);
if (jsonKey != null) {
// next the value will be a start object
generator.writeEndObject();
}
generator.writeEndObject();
}
private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModelGroupedNamed> void writeGroupedModelObject(
@NonNull T handler,
@NonNull IBoundObject parent,
@NonNull ObjectWriter<T> propertyWriter) throws IOException {
generator.writeStartObject();
IBoundInstanceModelChoiceGroup choiceGroup = handler.getParentContainer();
IBoundInstanceFlag jsonKey = choiceGroup.getItemJsonKey(parent);
if (jsonKey != null) {
Object keyValue = jsonKey.getValue(parent);
if (keyValue == null) {
throw new IOException(String.format("Null value for json-key for definition '%s'",
jsonKey.getContainingDefinition().toCoordinates()));
}
// the field will be the JSON key value
String key;
try {
key = jsonKey.getJavaTypeAdapter().asString(keyValue);
} catch (IllegalArgumentException ex) {
throw new IOException(
String.format("Invalid value '%s' for json-key for definition '%s'",
keyValue,
jsonKey.getContainingDefinition().toCoordinates()),
ex);
}
generator.writeFieldName(key);
// next the value will be a start object
generator.writeStartObject();
}
propertyWriter.accept(parent, handler);
if (jsonKey != null) {
// next the value will be a start object
generator.writeEndObject();
}
generator.writeEndObject();
}
/**
* Supports writing items that are {@link IBoundInstanceModel}-based.
*
* @param <ITEM>
* the Java type of the item
*/
private class ModelInstanceWriteHandler<ITEM>
extends AbstractModelInstanceWriteHandler<ITEM> {
public ModelInstanceWriteHandler(
@NonNull IBoundInstanceModel<ITEM> instance) {
super(instance);
}
@Override
public void writeList(List<ITEM> items) throws IOException {
IBoundInstanceModel<ITEM> instance = getCollectionInfo().getInstance();
boolean writeArray = false;
if (JsonGroupAsBehavior.LIST.equals(instance.getJsonGroupAsBehavior())
|| JsonGroupAsBehavior.SINGLETON_OR_LIST.equals(instance.getJsonGroupAsBehavior())
&& items.size() > 1) {
// write array, then items
writeArray = true;
generator.writeStartArray();
} // only other option is a singleton value, write item
super.writeList(items);
if (writeArray) {
// write the end array
generator.writeEndArray();
}
}
@Override
public void writeItem(ITEM item) throws IOException {
IBoundInstanceModel<ITEM> instance = getInstance();
instance.writeItem(item, MetaschemaJsonWriter.this);
}
}
}