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