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}