1
2
3
4
5
6 package dev.metaschema.databind.io.json;
7
8 import com.fasterxml.jackson.core.JsonGenerator;
9 import com.fasterxml.jackson.databind.JsonNode;
10 import com.fasterxml.jackson.databind.node.ObjectNode;
11
12 import org.eclipse.jdt.annotation.NotOwning;
13
14 import java.io.IOException;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Map;
18
19 import dev.metaschema.core.model.IAnyContent;
20 import dev.metaschema.core.model.IAnyInstance;
21 import dev.metaschema.core.model.IBoundObject;
22 import dev.metaschema.core.model.JsonGroupAsBehavior;
23 import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
24 import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
25 import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
26 import dev.metaschema.databind.model.IBoundFieldValue;
27 import dev.metaschema.databind.model.IBoundInstance;
28 import dev.metaschema.databind.model.IBoundInstanceFlag;
29 import dev.metaschema.databind.model.IBoundInstanceModel;
30 import dev.metaschema.databind.model.IBoundInstanceModelAny;
31 import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
32 import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
33 import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
34 import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
35 import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
36 import dev.metaschema.databind.model.IBoundInstanceModelGroupedField;
37 import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
38 import dev.metaschema.databind.model.IBoundProperty;
39 import dev.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
40 import dev.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
41 import dev.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
42 import dev.metaschema.databind.model.info.IItemWriteHandler;
43 import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
44 import edu.umd.cs.findbugs.annotations.NonNull;
45
46
47
48
49
50
51
52
53
54
55
56
57 @SuppressWarnings("PMD.CouplingBetweenObjects")
58 public class MetaschemaJsonWriter implements IJsonWritingContext, IItemWriteHandler {
59 @NonNull
60 @NotOwning
61 private final JsonGenerator generator;
62
63
64
65
66
67
68
69
70
71 public MetaschemaJsonWriter(@NonNull @NotOwning JsonGenerator generator) {
72 this.generator = generator;
73 }
74
75 @Override
76 public JsonGenerator getWriter() {
77 return generator;
78 }
79
80
81
82
83
84 @Override
85 public void write(
86 @NonNull IBoundDefinitionModelComplex definition,
87 @NonNull IBoundObject item) throws IOException {
88 definition.writeItem(item, this);
89 }
90
91
92
93
94
95 private <T> void writeInstance(
96 @NonNull IBoundProperty<T> instance,
97 @NonNull IBoundObject parentItem) throws IOException {
98 @SuppressWarnings("unchecked")
99 T value = (T) instance.getValue(parentItem);
100 if (value != null && !value.equals(instance.getResolvedDefaultValue())) {
101 generator.writeFieldName(instance.getJsonName());
102 instance.writeItem(value, this);
103 }
104 }
105
106 private <T> void writeModelInstance(
107 @NonNull IBoundInstanceModel<T> instance,
108 @NonNull Object parentItem) throws IOException {
109 Object value = instance.getValue(parentItem);
110 if (value != null) {
111
112
113
114 IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
115 if (!collectionInfo.isEmpty(value)) {
116 generator.writeFieldName(instance.getJsonName());
117 collectionInfo.writeItems(new ModelInstanceWriteHandler<>(instance), value);
118 }
119 }
120 }
121
122 @SuppressWarnings("PMD.NullAssignment")
123 private void writeFieldValue(@NonNull IBoundFieldValue fieldValue, @NonNull Object parentItem) throws IOException {
124 Object item = fieldValue.getValue(parentItem);
125
126
127 IBoundInstanceFlag jsonValueKey = fieldValue.getParentFieldDefinition().getJsonValueKeyFlagInstance();
128 if (item == null) {
129 if (jsonValueKey != null) {
130 item = fieldValue.getDefaultValue();
131 }
132 } else if (item.equals(fieldValue.getResolvedDefaultValue())) {
133
134 item = null;
135 }
136
137 if (item != null) {
138
139
140
141
142 String valueKeyName;
143 if (jsonValueKey != null) {
144 Object keyValue = jsonValueKey.getValue(parentItem);
145 if (keyValue == null) {
146 throw new IOException(String.format("Null value for json-value-key for definition '%s'",
147 jsonValueKey.getContainingDefinition().toCoordinates()));
148 }
149 try {
150
151 valueKeyName = jsonValueKey.getJavaTypeAdapter().asString(keyValue);
152 } catch (IllegalArgumentException ex) {
153 throw new IOException(
154 String.format("Invalid value '%s' for json-value-key for definition '%s'",
155 keyValue,
156 jsonValueKey.getContainingDefinition().toCoordinates()),
157 ex);
158 }
159 } else {
160 valueKeyName = fieldValue.getParentFieldDefinition().getEffectiveJsonValueKeyName();
161 }
162 generator.writeFieldName(valueKeyName);
163
164
165 writeItemFieldValue(item, fieldValue);
166 }
167 }
168
169 @Override
170 public void writeItemFlag(Object item, IBoundInstanceFlag instance) throws IOException {
171 writeScalarItem(item, instance);
172 }
173
174 @Override
175 public void writeItemField(Object item, IBoundInstanceModelFieldScalar instance) throws IOException {
176 writeScalarItem(item, instance);
177 }
178
179 @Override
180 public void writeItemField(IBoundObject item, IBoundInstanceModelFieldComplex instance) throws IOException {
181 writeModelObject(
182 instance,
183 item,
184 this::writeObjectProperties);
185 }
186
187 @Override
188 public void writeItemField(IBoundObject item, IBoundInstanceModelGroupedField instance) throws IOException {
189 writeGroupedModelObject(
190 instance,
191 item,
192 (parent, handler) -> {
193 writeDiscriminatorProperty(handler);
194 writeObjectProperties(parent, handler);
195 });
196 }
197
198 @Override
199 public void writeItemField(IBoundObject item, IBoundDefinitionModelFieldComplex definition) throws IOException {
200 writeDefinitionObject(
201 definition,
202 item,
203 (ObjectWriter<IBoundDefinitionModelFieldComplex>) this::writeObjectProperties);
204 }
205
206 @Override
207 public void writeItemFieldValue(Object item, IBoundFieldValue fieldValue) throws IOException {
208 fieldValue.getJavaTypeAdapter().writeJsonValue(item, generator);
209 }
210
211 @Override
212 public void writeItemAssembly(IBoundObject item, IBoundInstanceModelAssembly instance) throws IOException {
213 writeModelObject(instance, item, this::writeObjectProperties);
214 }
215
216 @Override
217 public void writeItemAssembly(IBoundObject item, IBoundInstanceModelGroupedAssembly instance) throws IOException {
218 writeGroupedModelObject(
219 instance,
220 item,
221 (parent, handler) -> {
222 writeDiscriminatorProperty(handler);
223 writeObjectProperties(parent, handler);
224 });
225 }
226
227 @Override
228 public void writeItemAssembly(IBoundObject item, IBoundDefinitionModelAssembly definition) throws IOException {
229 writeDefinitionObject(definition, item, this::writeObjectProperties);
230 }
231
232 @Override
233 public void writeChoiceGroupItem(IBoundObject item, IBoundInstanceModelChoiceGroup instance) throws IOException {
234 IBoundInstanceModelGroupedNamed actualInstance = instance.getItemInstance(item);
235 assert actualInstance != null;
236 actualInstance.writeItem(item, this);
237 }
238
239
240
241
242
243
244
245
246
247
248
249 private void writeScalarItem(@NonNull Object item, @NonNull IFeatureScalarItemValueHandler handler)
250 throws IOException {
251 handler.getJavaTypeAdapter().writeJsonValue(item, generator);
252 }
253
254 private <T extends IBoundInstanceModelGroupedNamed> void writeDiscriminatorProperty(
255 @NonNull T instance) throws IOException {
256
257 IBoundInstanceModelChoiceGroup choiceGroup = instance.getParentContainer();
258
259
260 String discriminatorProperty = choiceGroup.getJsonDiscriminatorProperty();
261 String discriminatorValue = instance.getEffectiveDisciminatorValue();
262
263 generator.writeStringField(discriminatorProperty, discriminatorValue);
264 }
265
266 private <T extends IFeatureComplexItemValueHandler> void writeObjectProperties(
267 @NonNull IBoundObject parent,
268 @NonNull T handler) throws IOException {
269 for (IBoundProperty<?> property : handler.getJsonProperties().values()) {
270 assert property != null;
271
272 if (property instanceof IBoundInstanceModel) {
273 writeModelInstance((IBoundInstanceModel<?>) property, parent);
274 } else if (property instanceof IBoundInstance) {
275 writeInstance(property, parent);
276 } else {
277 writeFieldValue((IBoundFieldValue) property, parent);
278 }
279 }
280
281
282 writeAnyContent(parent, handler);
283 }
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298 private void writeAnyContent(
299 @NonNull IBoundObject parent,
300 @NonNull IFeatureComplexItemValueHandler handler) throws IOException {
301 IBoundDefinitionModelComplex definition = handler.getDefinition();
302 if (definition instanceof IBoundDefinitionModelAssembly) {
303 IAnyInstance anyInstance
304 = ((IBoundDefinitionModelAssembly) definition).getModelContainer().getAnyInstance();
305 if (anyInstance instanceof IBoundInstanceModelAny) {
306 IBoundInstanceModelAny boundAny = (IBoundInstanceModelAny) anyInstance;
307 IAnyContent anyContent = boundAny.getAnyContent(parent);
308 if (anyContent instanceof JsonAnyContent) {
309 JsonAnyContent jsonAny = (JsonAnyContent) anyContent;
310 if (!jsonAny.isEmpty()) {
311 ObjectNode props = jsonAny.getProperties();
312 Iterator<Map.Entry<String, JsonNode>> fields = props.fields();
313 while (fields.hasNext()) {
314 Map.Entry<String, JsonNode> entry = fields.next();
315 generator.writeFieldName(entry.getKey());
316 generator.writeTree(entry.getValue());
317 }
318 }
319 }
320 }
321 }
322 }
323
324 private <T extends IFeatureComplexItemValueHandler> void writeDefinitionObject(
325 @NonNull T handler,
326 @NonNull IBoundObject parent,
327 @NonNull ObjectWriter<T> propertyWriter) throws IOException {
328 generator.writeStartObject();
329
330 propertyWriter.accept(parent, handler);
331 generator.writeEndObject();
332 }
333
334 private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModel<IBoundObject>>
335 void writeModelObject(
336 @NonNull T handler,
337 @NonNull IBoundObject parent,
338 @NonNull ObjectWriter<T> propertyWriter) throws IOException {
339 generator.writeStartObject();
340
341 IBoundInstanceFlag jsonKey = handler.getItemJsonKey(parent);
342 if (jsonKey != null) {
343 Object keyValue = jsonKey.getValue(parent);
344 if (keyValue == null) {
345 throw new IOException(
346 String.format("Null value for json-key for definition '%s'",
347 jsonKey.getContainingDefinition().toCoordinates()));
348 }
349
350
351 String key;
352 try {
353 key = jsonKey.getJavaTypeAdapter().asString(keyValue);
354 } catch (IllegalArgumentException ex) {
355 throw new IOException(
356 String.format("Illegal value '%s' for json-key for definition '%s'",
357 keyValue,
358 jsonKey.getContainingDefinition().toCoordinates()),
359 ex);
360 }
361 generator.writeFieldName(key);
362
363
364 generator.writeStartObject();
365 }
366
367 propertyWriter.accept(parent, handler);
368
369 if (jsonKey != null) {
370
371 generator.writeEndObject();
372 }
373 generator.writeEndObject();
374 }
375
376 private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModelGroupedNamed> void writeGroupedModelObject(
377 @NonNull T handler,
378 @NonNull IBoundObject parent,
379 @NonNull ObjectWriter<T> propertyWriter) throws IOException {
380 generator.writeStartObject();
381
382 IBoundInstanceModelChoiceGroup choiceGroup = handler.getParentContainer();
383 IBoundInstanceFlag jsonKey = choiceGroup.getItemJsonKey(parent);
384 if (jsonKey != null) {
385 Object keyValue = jsonKey.getValue(parent);
386 if (keyValue == null) {
387 throw new IOException(String.format("Null value for json-key for definition '%s'",
388 jsonKey.getContainingDefinition().toCoordinates()));
389 }
390
391
392 String key;
393 try {
394 key = jsonKey.getJavaTypeAdapter().asString(keyValue);
395 } catch (IllegalArgumentException ex) {
396 throw new IOException(
397 String.format("Invalid value '%s' for json-key for definition '%s'",
398 keyValue,
399 jsonKey.getContainingDefinition().toCoordinates()),
400 ex);
401 }
402 generator.writeFieldName(key);
403
404
405 generator.writeStartObject();
406 }
407
408 propertyWriter.accept(parent, handler);
409
410 if (jsonKey != null) {
411
412 generator.writeEndObject();
413 }
414 generator.writeEndObject();
415 }
416
417
418
419
420
421
422
423 private class ModelInstanceWriteHandler<ITEM>
424 extends AbstractModelInstanceWriteHandler<ITEM> {
425 public ModelInstanceWriteHandler(
426 @NonNull IBoundInstanceModel<ITEM> instance) {
427 super(instance);
428 }
429
430 @Override
431 public void writeList(List<ITEM> items) throws IOException {
432 IBoundInstanceModel<ITEM> instance = getCollectionInfo().getInstance();
433
434 boolean writeArray = false;
435 if (JsonGroupAsBehavior.LIST.equals(instance.getJsonGroupAsBehavior())
436 || JsonGroupAsBehavior.SINGLETON_OR_LIST.equals(instance.getJsonGroupAsBehavior())
437 && items.size() > 1) {
438
439 writeArray = true;
440 generator.writeStartArray();
441 }
442
443 super.writeList(items);
444
445 if (writeArray) {
446
447 generator.writeEndArray();
448 }
449 }
450
451 @Override
452 public void writeItem(ITEM item) throws IOException {
453 IBoundInstanceModel<ITEM> instance = getInstance();
454 instance.writeItem(item, MetaschemaJsonWriter.this);
455 }
456 }
457 }