1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind.io.json;
7   
8   import com.fasterxml.jackson.core.JsonGenerator;
9   
10  import gov.nist.secauto.metaschema.core.model.IBoundObject;
11  import gov.nist.secauto.metaschema.core.model.JsonGroupAsBehavior;
12  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
13  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
14  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
15  import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
16  import gov.nist.secauto.metaschema.databind.model.IBoundInstance;
17  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
18  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModel;
19  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelAssembly;
20  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
21  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldComplex;
22  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldScalar;
23  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
24  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField;
25  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
26  import gov.nist.secauto.metaschema.databind.model.IBoundProperty;
27  import gov.nist.secauto.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
28  import gov.nist.secauto.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
29  import gov.nist.secauto.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
30  import gov.nist.secauto.metaschema.databind.model.info.IItemWriteHandler;
31  import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo;
32  
33  import java.io.IOException;
34  import java.util.List;
35  
36  import edu.umd.cs.findbugs.annotations.NonNull;
37  
38  public class MetaschemaJsonWriter implements IJsonWritingContext, IItemWriteHandler {
39    @NonNull
40    private final JsonGenerator generator;
41  
42    /**
43     * Construct a new Module-aware JSON writer.
44     *
45     * @param generator
46     *          the JSON generator to write with
47     * @see DefaultJsonProblemHandler
48     */
49    public MetaschemaJsonWriter(@NonNull JsonGenerator generator) {
50      this.generator = generator;
51    }
52  
53    @Override
54    public JsonGenerator getWriter() {
55      return generator;
56    }
57  
58    // =====================================
59    // Entry point for top-level-definitions
60    // =====================================
61  
62    @Override
63    public void write(
64        @NonNull IBoundDefinitionModelComplex definition,
65        @NonNull IBoundObject item) throws IOException {
66      definition.writeItem(item, this);
67    }
68  
69    // ================
70    // Instance writers
71    // ================
72  
73    private <T> void writeInstance(
74        @NonNull IBoundProperty<T> instance,
75        @NonNull IBoundObject parentItem) throws IOException {
76      @SuppressWarnings("unchecked") T value = (T) instance.getValue(parentItem);
77      if (value != null && !value.equals(instance.getResolvedDefaultValue())) {
78        generator.writeFieldName(instance.getJsonName());
79        instance.writeItem(value, this);
80      }
81    }
82  
83    private <T> void writeModelInstance(
84        @NonNull IBoundInstanceModel<T> instance,
85        @NonNull Object parentItem) throws IOException {
86      Object value = instance.getValue(parentItem);
87      if (value != null) {
88        // this if is not strictly needed, since isEmpty will return false on a null
89        // value
90        // checking null here potentially avoids the expensive operation of instatiating
91        IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
92        if (!collectionInfo.isEmpty(value)) {
93          generator.writeFieldName(instance.getJsonName());
94          collectionInfo.writeItems(new ModelInstanceWriteHandler<>(instance), value);
95        }
96      }
97    }
98  
99    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 }