1
2
3
4
5
6 package dev.metaschema.databind.io.json;
7
8 import com.fasterxml.jackson.core.JsonGenerator;
9
10 import org.eclipse.jdt.annotation.NotOwning;
11
12 import java.io.IOException;
13 import java.util.List;
14
15 import dev.metaschema.core.model.IBoundObject;
16 import dev.metaschema.core.model.JsonGroupAsBehavior;
17 import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
18 import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
19 import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
20 import dev.metaschema.databind.model.IBoundFieldValue;
21 import dev.metaschema.databind.model.IBoundInstance;
22 import dev.metaschema.databind.model.IBoundInstanceFlag;
23 import dev.metaschema.databind.model.IBoundInstanceModel;
24 import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
25 import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
26 import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
27 import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
28 import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
29 import dev.metaschema.databind.model.IBoundInstanceModelGroupedField;
30 import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
31 import dev.metaschema.databind.model.IBoundProperty;
32 import dev.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
33 import dev.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
34 import dev.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
35 import dev.metaschema.databind.model.info.IItemWriteHandler;
36 import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
37 import edu.umd.cs.findbugs.annotations.NonNull;
38
39
40
41
42
43
44
45
46
47
48
49
50 @SuppressWarnings("PMD.CouplingBetweenObjects")
51 public class MetaschemaJsonWriter implements IJsonWritingContext, IItemWriteHandler {
52 @NonNull
53 @NotOwning
54 private final JsonGenerator generator;
55
56
57
58
59
60
61
62
63
64 public MetaschemaJsonWriter(@NonNull @NotOwning JsonGenerator generator) {
65 this.generator = generator;
66 }
67
68 @Override
69 public JsonGenerator getWriter() {
70 return generator;
71 }
72
73
74
75
76
77 @Override
78 public void write(
79 @NonNull IBoundDefinitionModelComplex definition,
80 @NonNull IBoundObject item) throws IOException {
81 definition.writeItem(item, this);
82 }
83
84
85
86
87
88 private <T> void writeInstance(
89 @NonNull IBoundProperty<T> instance,
90 @NonNull IBoundObject parentItem) throws IOException {
91 @SuppressWarnings("unchecked")
92 T value = (T) instance.getValue(parentItem);
93 if (value != null && !value.equals(instance.getResolvedDefaultValue())) {
94 generator.writeFieldName(instance.getJsonName());
95 instance.writeItem(value, this);
96 }
97 }
98
99 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
105
106
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
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
127 item = null;
128 }
129
130 if (item != null) {
131
132
133
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
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
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
234
235
236
237
238
239
240
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
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 {
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
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
315 generator.writeStartObject();
316 }
317
318 propertyWriter.accept(parent, handler);
319
320 if (jsonKey != null) {
321
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
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
356 generator.writeStartObject();
357 }
358
359 propertyWriter.accept(parent, handler);
360
361 if (jsonKey != null) {
362
363 generator.writeEndObject();
364 }
365 generator.writeEndObject();
366 }
367
368
369
370
371
372
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
390 writeArray = true;
391 generator.writeStartArray();
392 }
393
394 super.writeList(items);
395
396 if (writeArray) {
397
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 }