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