1
2
3
4
5
6 package dev.metaschema.schemagen.json.impl;
7
8 import com.fasterxml.jackson.databind.JsonNode;
9 import com.fasterxml.jackson.databind.node.ArrayNode;
10 import com.fasterxml.jackson.databind.node.BigIntegerNode;
11 import com.fasterxml.jackson.databind.node.BooleanNode;
12 import com.fasterxml.jackson.databind.node.DecimalNode;
13 import com.fasterxml.jackson.databind.node.DoubleNode;
14 import com.fasterxml.jackson.databind.node.IntNode;
15 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
16 import com.fasterxml.jackson.databind.node.LongNode;
17 import com.fasterxml.jackson.databind.node.ObjectNode;
18 import com.fasterxml.jackson.databind.node.TextNode;
19
20 import java.math.BigDecimal;
21 import java.math.BigInteger;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Comparator;
25 import java.util.LinkedList;
26 import java.util.List;
27 import java.util.Set;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
30
31 import dev.metaschema.core.datatype.IDataTypeAdapter;
32 import dev.metaschema.core.datatype.markup.MarkupLine;
33 import dev.metaschema.core.datatype.markup.MarkupMultiline;
34 import dev.metaschema.core.model.IChoiceInstance;
35 import dev.metaschema.core.model.IContainerModelAbsolute;
36 import dev.metaschema.core.model.IFlagInstance;
37 import dev.metaschema.core.model.IModelDefinition;
38 import dev.metaschema.core.model.IModelElement;
39 import dev.metaschema.core.model.INamedModelElement;
40 import dev.metaschema.core.model.IValuedDefinition;
41 import dev.metaschema.core.model.IValuedInstance;
42 import dev.metaschema.core.qname.IEnhancedQName;
43 import dev.metaschema.core.util.CollectionUtil;
44 import dev.metaschema.core.util.ObjectUtils;
45 import edu.umd.cs.findbugs.annotations.NonNull;
46 import edu.umd.cs.findbugs.annotations.Nullable;
47
48
49
50
51
52
53
54
55 public final class JsonSchemaHelper {
56
57
58
59 public static final Comparator<IJsonSchemaPropertyNamed> INSTANCE_NAME_COMPARATOR
60 = Comparator.comparing(IJsonSchemaPropertyNamed::getName);
61
62
63
64 public static final Comparator<IJsonSchemaDefinable> DEFINABLE_NAME_COMPARATOR
65 = Comparator.comparing(IJsonSchemaDefinable::getDefinitionName);
66
67
68
69
70
71
72
73
74
75 public static void generateTitle(
76 @NonNull INamedModelElement named,
77 @NonNull ObjectNode obj) {
78 String formalName = named.getEffectiveFormalName();
79 if (formalName != null) {
80 obj.put("title", formalName);
81 }
82 }
83
84
85
86
87
88
89
90
91
92
93
94
95 public static <NAMED extends INamedModelElement & IModelElement> void generateDescription(
96 @NonNull NAMED named,
97 @NonNull ObjectNode obj) {
98 MarkupLine description = named.getEffectiveDescription();
99
100 StringBuilder retval = null;
101 if (description != null) {
102 retval = new StringBuilder().append(description.toMarkdown());
103 }
104
105 MarkupMultiline remarks = named.getRemarks();
106 if (remarks != null) {
107 if (retval == null) {
108 retval = new StringBuilder();
109 } else {
110 retval.append("\n\n");
111 }
112 retval.append(remarks.toMarkdown());
113 }
114 if (retval != null) {
115 obj.put("description", retval.toString());
116 }
117 }
118
119
120
121
122
123
124
125
126
127
128 public static void generateDefault(
129 @NonNull IValuedInstance instance,
130 @NonNull ObjectNode obj) {
131 Object defaultValue = instance.getEffectiveDefaultValue();
132 if (defaultValue != null) {
133 IValuedDefinition definition = instance.getDefinition();
134 IDataTypeAdapter<?> adapter = definition.getJavaTypeAdapter();
135 obj.set("default", toJsonValue(defaultValue, adapter));
136 }
137 }
138
139 private static JsonNode toJsonValue(
140 @Nullable Object defaultValue,
141 @NonNull IDataTypeAdapter<?> adapter) {
142 JsonNode retval = null;
143 switch (adapter.getJsonRawType()) {
144 case BOOLEAN:
145 if (defaultValue instanceof Boolean) {
146 retval = BooleanNode.valueOf((Boolean) defaultValue);
147 }
148 break;
149 case INTEGER:
150 if (defaultValue instanceof BigInteger) {
151 retval = BigIntegerNode.valueOf((BigInteger) defaultValue);
152 } else if (defaultValue instanceof Integer) {
153 retval = IntNode.valueOf((Integer) defaultValue);
154 } else if (defaultValue instanceof Long) {
155 retval = LongNode.valueOf((Long) defaultValue);
156 }
157 break;
158 case NUMBER:
159 if (defaultValue instanceof BigDecimal) {
160 retval = DecimalNode.valueOf((BigDecimal) defaultValue);
161 } else if (defaultValue instanceof Double) {
162 retval = DoubleNode.valueOf((Double) defaultValue);
163 }
164 break;
165 case ANY:
166 case ARRAY:
167 case OBJECT:
168 case NULL:
169 throw new UnsupportedOperationException("Invalid type: " + adapter.getClass());
170 case STRING:
171
172 break;
173 }
174
175 if (retval == null && defaultValue != null) {
176 retval = TextNode.valueOf(adapter.asString(defaultValue));
177 }
178 return retval;
179 }
180
181
182
183
184
185
186
187
188
189 @NonNull
190 public static String generateDefinitionJsonPointer(@NonNull IJsonSchemaDefinable schema) {
191 return ObjectUtils.notNull(new StringBuilder()
192 .append("#/definitions/")
193 .append(schema.getDefinitionName())
194 .toString());
195 }
196
197
198
199
200
201
202
203
204
205
206
207 public static void generateProperties(
208 @NonNull Collection<IJsonSchemaPropertyNamed> properties,
209 @NonNull ObjectNode node,
210 @NonNull IJsonGenerationState state) {
211
212 if (!properties.isEmpty()) {
213 ObjectNode propertiesNode = node.putObject("properties");
214
215 List<String> required = new LinkedList<>();
216 properties.forEach(property -> {
217 ObjectNode propertyNode = ObjectUtils.notNull(state.getJsonNodeFactory().objectNode());
218 property.generate(propertyNode, state);
219
220 String name = property.getName();
221 propertiesNode.set(name, propertyNode);
222
223 if (property.isRequired()) {
224 required.add(name);
225 }
226 });
227
228 if (!required.isEmpty()) {
229 ArrayNode requiredNode = node.putArray("required");
230 required.forEach(requiredNode::add);
231 }
232 }
233 }
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251 @NonNull
252 public static List<IJsonSchemaPropertyFlag> buildFlagProperties(
253 @NonNull IModelDefinition definition,
254 @Nullable IEnhancedQName jsonKeyFlagName,
255 @NonNull IJsonGenerationState state) {
256
257 Stream<? extends IFlagInstance> flagStream = definition.getFlagInstances().stream();
258
259
260 if (jsonKeyFlagName != null) {
261 IFlagInstance jsonKeyFlag = definition.getFlagInstanceByName(jsonKeyFlagName.getIndexPosition());
262 if (jsonKeyFlag == null) {
263 throw new IllegalArgumentException(
264 String.format("The referenced json-key flag-name '%s' does not exist on definition '%s'.",
265 jsonKeyFlagName,
266 definition.getName()));
267 }
268 flagStream = flagStream.filter(instance -> !jsonKeyFlag.equals(instance));
269 }
270
271 return ObjectUtils.notNull(flagStream
272 .map(instance -> state.getJsonSchemaPropertyFlag(ObjectUtils.requireNonNull(instance)))
273 .collect(Collectors.toUnmodifiableList()));
274 }
275
276
277
278
279
280
281
282
283
284
285
286
287
288 @NonNull
289 public static List<IJsonSchemaPropertyNamed> buildModelProperties(
290 @NonNull IContainerModelAbsolute definition,
291 @NonNull IJsonGenerationState state) {
292 return ObjectUtils.notNull(definition.getModelInstances().stream()
293
294 .filter(instance -> !(instance instanceof IChoiceInstance))
295 .map(instance -> state.getJsonSchemaPropertyModel(ObjectUtils.notNull(instance)))
296 .collect(Collectors.toUnmodifiableList()));
297 }
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313 public static void generateFieldBody(
314 @NonNull IJsonSchemaDefinitionField field,
315 @NonNull ObjectNode node,
316 @NonNull IJsonGenerationState state) {
317 if (field.getNonValueProperties().isEmpty()) {
318
319 field.getFieldValue().generateJsonSchemaOrDefinitionRef(node, state);
320 } else {
321 generateComplexFieldBody(field, node, state);
322 }
323 }
324
325 private static void generateComplexFieldBody(
326 @NonNull IJsonSchemaDefinitionField field,
327 @NonNull ObjectNode node,
328 @NonNull IJsonGenerationState state) {
329 List<? extends IJsonSchemaPropertyNamed> nonValueProperties = field.getNonValueProperties();
330 node.put("type", "object");
331
332 IFlagInstance jsonValueKeyFlag = field.getDefinition().getJsonValueKeyFlagInstance();
333
334 Stream<? extends IJsonSchemaPropertyNamed> propertiesStream = nonValueProperties.stream();
335
336 if (jsonValueKeyFlag == null) {
337
338 propertiesStream = Stream.concat(propertiesStream, Stream.of(new FieldValueProperty(field)));
339 } else {
340 propertiesStream = propertiesStream.filter(property -> !(property instanceof IJsonSchemaPropertyFlag)
341 || !jsonValueKeyFlag.equals(((IJsonSchemaPropertyFlag) property).getInstance()));
342 }
343
344 List<IJsonSchemaPropertyNamed> properties = ObjectUtils.notNull(propertiesStream
345 .sorted(INSTANCE_NAME_COMPARATOR)
346 .collect(Collectors.toUnmodifiableList()));
347 generateProperties(
348 properties,
349 node,
350 state);
351
352 if (jsonValueKeyFlag == null) {
353 node.put("additionalProperties", false);
354 } else {
355 ObjectNode additionalPropertiesTypeNode;
356
357 additionalPropertiesTypeNode = ObjectUtils.notNull(JsonNodeFactory.instance.objectNode());
358
359 field.getFieldValue().generateJsonSchemaOrDefinitionRef(additionalPropertiesTypeNode, state);
360
361 ObjectNode additionalPropertiesNode = ObjectUtils.notNull(JsonNodeFactory.instance.objectNode());
362 ArrayNode allOf = additionalPropertiesNode.putArray("allOf");
363 allOf.add(additionalPropertiesTypeNode);
364 allOf.addObject()
365 .put(
366 "minProperties",
367
368 properties.stream()
369 .filter(IJsonSchemaPropertyNamed::isRequired)
370 .count() + 1)
371 .put("maxProperties", properties.size() + 1);
372
373 node.set("additionalProperties", additionalPropertiesNode);
374 }
375 }
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390 public static void generateAssemblyBody(
391 @NonNull IJsonSchemaDefinitionAssembly assembly,
392 @NonNull ObjectNode node,
393 @NonNull IJsonGenerationState state) {
394 node.put("type", "object");
395
396 List<JsonSchemaHelper.Choice> availableChoices = assembly.getChoices();
397
398 if (availableChoices.size() == 1) {
399 generateProperties(
400 availableChoices.iterator().next().getCombinations(),
401 node,
402 state);
403 node.put("additionalProperties", false);
404 } else if (availableChoices.size() > 1) {
405 ArrayNode oneOf = node.putArray("anyOf");
406 availableChoices.forEach(choice -> {
407 ObjectNode schemaNode = ObjectUtils.notNull(oneOf.addObject());
408
409 generateProperties(
410 choice.getCombinations(),
411 schemaNode,
412 state);
413 schemaNode.put("additionalProperties", false);
414 });
415 }
416 }
417
418 private static class FieldValueProperty implements IJsonSchemaPropertyNamed {
419 private final IJsonSchemaDefinitionField field;
420
421 public FieldValueProperty(IJsonSchemaDefinitionField field) {
422 this.field = field;
423 }
424
425 @Override
426 public String getName() {
427 return field.getDefinition().getEffectiveJsonValueKeyName();
428 }
429
430 @Override
431 public Stream<IJsonSchemaDefinable> collectDefinitions(
432 Set<IJsonSchemaDefinitionAssembly> visited,
433 IJsonGenerationState state) {
434 return ObjectUtils.notNull(Stream.empty());
435 }
436
437 @Override
438 public void generate(ObjectNode node, IJsonGenerationState state) {
439 field.getFieldValue().generateJsonSchemaOrDefinitionRef(ObjectUtils.notNull(node.putObject(getName())), state);
440 }
441
442 @Override
443 public boolean isRequired() {
444 return true;
445 }
446 }
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462 @NonNull
463 public static Stream<Choice> explodeChoices(
464 @NonNull Choice baseChoice,
465 @NonNull List<? extends IChoiceInstance> choiceInstances,
466 @NonNull IJsonGenerationState state) {
467 Stream<Choice> retval = Stream.of(baseChoice);
468 for (IChoiceInstance choice : choiceInstances) {
469 List<IJsonSchemaPropertyNamed> newChoices = buildModelProperties(ObjectUtils.notNull(choice), state);
470 retval = retval.flatMap(oldChoice -> oldChoice.explode(newChoices));
471 }
472 return ObjectUtils.notNull(retval);
473 }
474
475
476
477
478
479
480
481 public static final class Choice {
482 @NonNull
483 private final List<IJsonSchemaPropertyNamed> combinations;
484
485
486
487
488
489
490
491 public Choice(@NonNull List<IJsonSchemaPropertyNamed> combinations) {
492 this.combinations = combinations;
493 }
494
495
496
497
498
499
500 @NonNull
501 public List<IJsonSchemaPropertyNamed> getCombinations() {
502 return combinations;
503 }
504
505
506
507
508
509
510
511
512
513
514
515 @NonNull
516 public Stream<Choice> explode(@NonNull List<IJsonSchemaPropertyNamed> newChoices) {
517 return ObjectUtils.notNull(newChoices.isEmpty()
518 ? Stream.of(this)
519 : newChoices.stream()
520 .map(next -> {
521 List<IJsonSchemaPropertyNamed> retval = new ArrayList<>(combinations.size() + 1);
522 retval.addAll(combinations);
523 retval.add(next);
524 return new Choice(CollectionUtil.unmodifiableList(retval));
525 }));
526 }
527 }
528
529 private JsonSchemaHelper() {
530
531 }
532 }