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  @SuppressWarnings("PMD.CouplingBetweenObjects")
39  public class MetaschemaJsonWriter implements IJsonWritingContext, IItemWriteHandler {
40    @NonNull
41    private final JsonGenerator generator;
42  
43    /**
44     * Construct a new Module-aware JSON writer.
45     *
46     * @param generator
47     *          the JSON generator to write with
48     * @see DefaultJsonProblemHandler
49     */
50    public MetaschemaJsonWriter(@NonNull JsonGenerator generator) {
51      this.generator = generator;
52    }
53  
54    @Override
55    public JsonGenerator getWriter() {
56      return generator;
57    }
58  
59    // =====================================
60    // Entry point for top-level-definitions
61    // =====================================
62  
63    @Override
64    public void write(
65        @NonNull IBoundDefinitionModelComplex definition,
66        @NonNull IBoundObject item) throws IOException {
67      definition.writeItem(item, this);
68    }
69  
70    // ================
71    // Instance writers
72    // ================
73  
74    private <T> void writeInstance(
75        @NonNull IBoundProperty<T> instance,
76        @NonNull IBoundObject parentItem) throws IOException {
77      @SuppressWarnings("unchecked")
78      T value = (T) instance.getValue(parentItem);
79      if (value != null && !value.equals(instance.getResolvedDefaultValue())) {
80        generator.writeFieldName(instance.getJsonName());
81        instance.writeItem(value, this);
82      }
83    }
84  
85    private <T> void writeModelInstance(
86        @NonNull IBoundInstanceModel<T> instance,
87        @NonNull Object parentItem) throws IOException {
88      Object value = instance.getValue(parentItem);
89      if (value != null) {
90        // this if is not strictly needed, since isEmpty will return false on a null
91        // value
92        // checking null here potentially avoids the expensive operation of instatiating
93        IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
94        if (!collectionInfo.isEmpty(value)) {
95          generator.writeFieldName(instance.getJsonName());
96          collectionInfo.writeItems(new ModelInstanceWriteHandler<>(instance), value);
97        }
98      }
99    }
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 }