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}