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.JsonGenerator;
009
010import gov.nist.secauto.metaschema.core.model.IBoundObject;
011import gov.nist.secauto.metaschema.core.model.JsonGroupAsBehavior;
012import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
013import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
014import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
015import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
016import gov.nist.secauto.metaschema.databind.model.IBoundInstance;
017import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
018import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModel;
019import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelAssembly;
020import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
021import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldComplex;
022import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldScalar;
023import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
024import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField;
025import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
026import gov.nist.secauto.metaschema.databind.model.IBoundProperty;
027import gov.nist.secauto.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
028import gov.nist.secauto.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
029import gov.nist.secauto.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
030import gov.nist.secauto.metaschema.databind.model.info.IItemWriteHandler;
031import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo;
032
033import java.io.IOException;
034import java.util.List;
035
036import edu.umd.cs.findbugs.annotations.NonNull;
037
038public class MetaschemaJsonWriter implements IJsonWritingContext, IItemWriteHandler {
039  @NonNull
040  private final JsonGenerator generator;
041
042  /**
043   * Construct a new Module-aware JSON writer.
044   *
045   * @param generator
046   *          the JSON generator to write with
047   * @see DefaultJsonProblemHandler
048   */
049  public MetaschemaJsonWriter(@NonNull JsonGenerator generator) {
050    this.generator = generator;
051  }
052
053  @Override
054  public JsonGenerator getWriter() {
055    return generator;
056  }
057
058  // =====================================
059  // Entry point for top-level-definitions
060  // =====================================
061
062  @Override
063  public void write(
064      @NonNull IBoundDefinitionModelComplex definition,
065      @NonNull IBoundObject item) throws IOException {
066    definition.writeItem(item, this);
067  }
068
069  // ================
070  // Instance writers
071  // ================
072
073  private <T> void writeInstance(
074      @NonNull IBoundProperty<T> instance,
075      @NonNull IBoundObject parentItem) throws IOException {
076    @SuppressWarnings("unchecked") T value = (T) instance.getValue(parentItem);
077    if (value != null && !value.equals(instance.getResolvedDefaultValue())) {
078      generator.writeFieldName(instance.getJsonName());
079      instance.writeItem(value, this);
080    }
081  }
082
083  private <T> void writeModelInstance(
084      @NonNull IBoundInstanceModel<T> instance,
085      @NonNull Object parentItem) throws IOException {
086    Object value = instance.getValue(parentItem);
087    if (value != null) {
088      // this if is not strictly needed, since isEmpty will return false on a null
089      // value
090      // checking null here potentially avoids the expensive operation of instatiating
091      IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
092      if (!collectionInfo.isEmpty(value)) {
093        generator.writeFieldName(instance.getJsonName());
094        collectionInfo.writeItems(new ModelInstanceWriteHandler<>(instance), value);
095      }
096    }
097  }
098
099  private void writeFieldValue(@NonNull IBoundFieldValue fieldValue, @NonNull Object parentItem) throws IOException {
100    Object item = fieldValue.getValue(parentItem);
101
102    // handle json value key
103    IBoundInstanceFlag jsonValueKey = fieldValue.getParentFieldDefinition().getJsonValueKeyFlagInstance();
104    if (item == null) {
105      if (jsonValueKey != null) {
106        item = fieldValue.getDefaultValue();
107      }
108    } else if (item.equals(fieldValue.getResolvedDefaultValue())) {
109      // same as default
110      item = null;
111    }
112
113    if (item != null) {
114      // There are two modes:
115      // 1) use of a JSON value key, or
116      // 2) a simple value named "value"
117
118      String valueKeyName;
119      if (jsonValueKey != null) {
120        Object keyValue = jsonValueKey.getValue(parentItem);
121        if (keyValue == null) {
122          throw new IOException(String.format("Null value for json-value-key for definition '%s'",
123              jsonValueKey.getContainingDefinition().toCoordinates()));
124        }
125        // this is the JSON value key case
126        valueKeyName = jsonValueKey.getJavaTypeAdapter().asString(keyValue);
127      } else {
128        valueKeyName = fieldValue.getParentFieldDefinition().getEffectiveJsonValueKeyName();
129      }
130      generator.writeFieldName(valueKeyName);
131      // LOGGER.info("FIELD: {}", valueKeyName);
132
133      writeItemFieldValue(item, fieldValue);
134    }
135  }
136
137  @Override
138  public void writeItemFlag(Object item, IBoundInstanceFlag instance) throws IOException {
139    writeScalarItem(item, instance);
140  }
141
142  @Override
143  public void writeItemField(Object item, IBoundInstanceModelFieldScalar instance) throws IOException {
144    writeScalarItem(item, instance);
145  }
146
147  @Override
148  public void writeItemField(IBoundObject item, IBoundInstanceModelFieldComplex instance) throws IOException {
149    writeModelObject(
150        instance,
151        item,
152        this::writeObjectProperties);
153  }
154
155  @Override
156  public void writeItemField(IBoundObject item, IBoundInstanceModelGroupedField instance) throws IOException {
157    writeGroupedModelObject(
158        instance,
159        item,
160        (parent, handler) -> {
161          writeDiscriminatorProperty(handler);
162          writeObjectProperties(parent, handler);
163        });
164  }
165
166  @Override
167  public void writeItemField(IBoundObject item, IBoundDefinitionModelFieldComplex definition) throws IOException {
168    writeDefinitionObject(
169        definition,
170        item,
171        (ObjectWriter<IBoundDefinitionModelFieldComplex>) this::writeObjectProperties);
172  }
173
174  @Override
175  public void writeItemFieldValue(Object item, IBoundFieldValue fieldValue) throws IOException {
176    fieldValue.getJavaTypeAdapter().writeJsonValue(item, generator);
177  }
178
179  @Override
180  public void writeItemAssembly(IBoundObject item, IBoundInstanceModelAssembly instance) throws IOException {
181    writeModelObject(instance, item, this::writeObjectProperties);
182  }
183
184  @Override
185  public void writeItemAssembly(IBoundObject item, IBoundInstanceModelGroupedAssembly instance) throws IOException {
186    writeGroupedModelObject(
187        instance,
188        item,
189        (parent, handler) -> {
190          writeDiscriminatorProperty(handler);
191          writeObjectProperties(parent, handler);
192        });
193  }
194
195  @Override
196  public void writeItemAssembly(IBoundObject item, IBoundDefinitionModelAssembly definition) throws IOException {
197    writeDefinitionObject(definition, item, this::writeObjectProperties);
198  }
199
200  @Override
201  public void writeChoiceGroupItem(IBoundObject item, IBoundInstanceModelChoiceGroup instance) throws IOException {
202    IBoundInstanceModelGroupedNamed actualInstance = instance.getItemInstance(item);
203    assert actualInstance != null;
204    actualInstance.writeItem(item, this);
205  }
206
207  /**
208   * Writes a scalar item.
209   *
210   * @param item
211   *          the item to write
212   * @param handler
213   *          the value handler
214   * @throws IOException
215   *           if an error occurred while writing the scalar value
216   */
217  private void writeScalarItem(@NonNull Object item, @NonNull IFeatureScalarItemValueHandler handler)
218      throws IOException {
219    handler.getJavaTypeAdapter().writeJsonValue(item, generator);
220  }
221
222  private <T extends IBoundInstanceModelGroupedNamed> void writeDiscriminatorProperty(
223      @NonNull T instance) throws IOException {
224
225    IBoundInstanceModelChoiceGroup choiceGroup = instance.getParentContainer();
226
227    // write JSON object discriminator
228    String discriminatorProperty = choiceGroup.getJsonDiscriminatorProperty();
229    String discriminatorValue = instance.getEffectiveDisciminatorValue();
230
231    generator.writeStringField(discriminatorProperty, discriminatorValue);
232  }
233
234  private <T extends IFeatureComplexItemValueHandler> void writeObjectProperties(
235      @NonNull IBoundObject parent,
236      @NonNull T handler) throws IOException {
237    for (IBoundProperty<?> property : handler.getJsonProperties().values()) {
238      assert property != null;
239
240      if (property instanceof IBoundInstanceModel) {
241        writeModelInstance((IBoundInstanceModel<?>) property, parent);
242      } else if (property instanceof IBoundInstance) {
243        writeInstance(property, parent);
244      } else { // IBoundFieldValue
245        writeFieldValue((IBoundFieldValue) property, parent);
246      }
247    }
248  }
249
250  private <T extends IFeatureComplexItemValueHandler> void writeDefinitionObject(
251      @NonNull T handler,
252      @NonNull IBoundObject parent,
253      @NonNull ObjectWriter<T> propertyWriter) throws IOException {
254    generator.writeStartObject();
255
256    propertyWriter.accept(parent, handler);
257    generator.writeEndObject();
258  }
259
260  private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModel<IBoundObject>>
261      void writeModelObject(
262          @NonNull T handler,
263          @NonNull IBoundObject parent,
264          @NonNull ObjectWriter<T> propertyWriter) throws IOException {
265    generator.writeStartObject();
266
267    IBoundInstanceFlag jsonKey = handler.getItemJsonKey(parent);
268    if (jsonKey != null) {
269      Object keyValue = jsonKey.getValue(parent);
270      if (keyValue == null) {
271        throw new IOException(String.format("Null value for json-key for definition '%s'",
272            jsonKey.getContainingDefinition().toCoordinates()));
273      }
274
275      // the field will be the JSON key value
276      String key = jsonKey.getJavaTypeAdapter().asString(keyValue);
277      generator.writeFieldName(key);
278
279      // next the value will be a start object
280      generator.writeStartObject();
281    }
282
283    propertyWriter.accept(parent, handler);
284
285    if (jsonKey != null) {
286      // next the value will be a start object
287      generator.writeEndObject();
288    }
289    generator.writeEndObject();
290  }
291
292  private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModelGroupedNamed> void writeGroupedModelObject(
293      @NonNull T handler,
294      @NonNull IBoundObject parent,
295      @NonNull ObjectWriter<T> propertyWriter) throws IOException {
296    generator.writeStartObject();
297
298    IBoundInstanceModelChoiceGroup choiceGroup = handler.getParentContainer();
299    IBoundInstanceFlag jsonKey = choiceGroup.getItemJsonKey(parent);
300    if (jsonKey != null) {
301      Object keyValue = jsonKey.getValue(parent);
302      if (keyValue == null) {
303        throw new IOException(String.format("Null value for json-key for definition '%s'",
304            jsonKey.getContainingDefinition().toCoordinates()));
305      }
306
307      // the field will be the JSON key value
308      String key = jsonKey.getJavaTypeAdapter().asString(keyValue);
309      generator.writeFieldName(key);
310
311      // next the value will be a start object
312      generator.writeStartObject();
313    }
314
315    propertyWriter.accept(parent, handler);
316
317    if (jsonKey != null) {
318      // next the value will be a start object
319      generator.writeEndObject();
320    }
321    generator.writeEndObject();
322  }
323
324  /**
325   * Supports writing items that are {@link IBoundInstanceModel}-based.
326   *
327   * @param <ITEM>
328   *          the Java type of the item
329   */
330  private class ModelInstanceWriteHandler<ITEM>
331      extends AbstractModelInstanceWriteHandler<ITEM> {
332    public ModelInstanceWriteHandler(
333        @NonNull IBoundInstanceModel<ITEM> instance) {
334      super(instance);
335    }
336
337    @Override
338    public void writeList(List<ITEM> items) throws IOException {
339      IBoundInstanceModel<ITEM> instance = getCollectionInfo().getInstance();
340
341      boolean writeArray = false;
342      if (JsonGroupAsBehavior.LIST.equals(instance.getJsonGroupAsBehavior())
343          || JsonGroupAsBehavior.SINGLETON_OR_LIST.equals(instance.getJsonGroupAsBehavior())
344              && items.size() > 1) {
345        // write array, then items
346        writeArray = true;
347        generator.writeStartArray();
348      } // only other option is a singleton value, write item
349
350      super.writeList(items);
351
352      if (writeArray) {
353        // write the end array
354        generator.writeEndArray();
355      }
356    }
357
358    @Override
359    public void writeItem(ITEM item) throws IOException {
360      IBoundInstanceModel<ITEM> instance = getInstance();
361      instance.writeItem(item, MetaschemaJsonWriter.this);
362    }
363  }
364}