1
2
3
4
5
6 package gov.nist.secauto.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 gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
21 import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
22 import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
23 import gov.nist.secauto.metaschema.core.model.IChoiceInstance;
24 import gov.nist.secauto.metaschema.core.model.IContainerModelAbsolute;
25 import gov.nist.secauto.metaschema.core.model.IFlagInstance;
26 import gov.nist.secauto.metaschema.core.model.IModelDefinition;
27 import gov.nist.secauto.metaschema.core.model.IModelElement;
28 import gov.nist.secauto.metaschema.core.model.INamedModelElement;
29 import gov.nist.secauto.metaschema.core.model.IValuedDefinition;
30 import gov.nist.secauto.metaschema.core.model.IValuedInstance;
31 import gov.nist.secauto.metaschema.core.qname.IEnhancedQName;
32 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
33 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
34
35 import java.math.BigDecimal;
36 import java.math.BigInteger;
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.Comparator;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Set;
43 import java.util.stream.Collectors;
44 import java.util.stream.Stream;
45
46 import edu.umd.cs.findbugs.annotations.NonNull;
47 import edu.umd.cs.findbugs.annotations.Nullable;
48
49 public final class JsonSchemaHelper {
50
51
52
53 public static final Comparator<IJsonSchemaPropertyNamed> INSTANCE_NAME_COMPARATOR
54 = Comparator.comparing(IJsonSchemaPropertyNamed::getName);
55
56
57
58 public static final Comparator<IJsonSchemaDefinable> DEFINABLE_NAME_COMPARATOR
59 = Comparator.comparing(IJsonSchemaDefinable::getDefinitionName);
60
61 public static void generateTitle(
62 @NonNull INamedModelElement named,
63 @NonNull ObjectNode obj) {
64 String formalName = named.getEffectiveFormalName();
65 if (formalName != null) {
66 obj.put("title", formalName);
67 }
68 }
69
70 public static <NAMED extends INamedModelElement & IModelElement> void generateDescription(
71 @NonNull NAMED named,
72 @NonNull ObjectNode obj) {
73 MarkupLine description = named.getEffectiveDescription();
74
75 StringBuilder retval = null;
76 if (description != null) {
77 retval = new StringBuilder().append(description.toMarkdown());
78 }
79
80 MarkupMultiline remarks = named.getRemarks();
81 if (remarks != null) {
82 if (retval == null) {
83 retval = new StringBuilder();
84 } else {
85 retval.append("\n\n");
86 }
87 retval.append(remarks.toMarkdown());
88 }
89 if (retval != null) {
90 obj.put("description", retval.toString());
91 }
92 }
93
94 public static void generateDefault(
95 @NonNull IValuedInstance instance,
96 @NonNull ObjectNode obj) {
97 Object defaultValue = instance.getEffectiveDefaultValue();
98 if (defaultValue != null) {
99 IValuedDefinition definition = instance.getDefinition();
100 IDataTypeAdapter<?> adapter = definition.getJavaTypeAdapter();
101 obj.set("default", toJsonValue(defaultValue, adapter));
102 }
103 }
104
105 private static JsonNode toJsonValue(
106 @Nullable Object defaultValue,
107 @NonNull IDataTypeAdapter<?> adapter) {
108 JsonNode retval = null;
109 switch (adapter.getJsonRawType()) {
110 case BOOLEAN:
111 if (defaultValue instanceof Boolean) {
112 retval = BooleanNode.valueOf((Boolean) defaultValue);
113 }
114 break;
115 case INTEGER:
116 if (defaultValue instanceof BigInteger) {
117 retval = BigIntegerNode.valueOf((BigInteger) defaultValue);
118 } else if (defaultValue instanceof Integer) {
119 retval = IntNode.valueOf((Integer) defaultValue);
120 } else if (defaultValue instanceof Long) {
121 retval = LongNode.valueOf((Long) defaultValue);
122 }
123 break;
124 case NUMBER:
125 if (defaultValue instanceof BigDecimal) {
126 retval = DecimalNode.valueOf((BigDecimal) defaultValue);
127 } else if (defaultValue instanceof Double) {
128 retval = DoubleNode.valueOf((Double) defaultValue);
129 }
130 break;
131 case ANY:
132 case ARRAY:
133 case OBJECT:
134 case NULL:
135 throw new UnsupportedOperationException("Invalid type: " + adapter.getClass());
136 case STRING:
137
138 break;
139 }
140
141 if (retval == null && defaultValue != null) {
142 retval = TextNode.valueOf(adapter.asString(defaultValue));
143 }
144 return retval;
145 }
146
147
148
149
150
151
152
153
154
155 @NonNull
156 public static String generateDefinitionJsonPointer(@NonNull IJsonSchemaDefinable schema) {
157 return ObjectUtils.notNull(new StringBuilder()
158 .append("#/definitions/")
159 .append(schema.getDefinitionName())
160 .toString());
161 }
162
163 public static void generateProperties(
164 @NonNull Collection<IJsonSchemaPropertyNamed> properties,
165 @NonNull ObjectNode node,
166 @NonNull IJsonGenerationState state) {
167
168 if (!properties.isEmpty()) {
169 ObjectNode propertiesNode = node.putObject("properties");
170
171 List<String> required = new LinkedList<>();
172 properties.forEach(property -> {
173 ObjectNode propertyNode = ObjectUtils.notNull(state.getJsonNodeFactory().objectNode());
174 property.generate(propertyNode, state);
175
176 String name = property.getName();
177 propertiesNode.set(name, propertyNode);
178
179 if (property.isRequired()) {
180 required.add(name);
181 }
182 });
183
184 if (!required.isEmpty()) {
185 ArrayNode requiredNode = node.putArray("required");
186 required.forEach(requiredNode::add);
187 }
188 }
189 }
190
191 @NonNull
192 public static List<IJsonSchemaPropertyFlag> buildFlagProperties(
193 @NonNull IModelDefinition definition,
194 @Nullable IEnhancedQName jsonKeyFlagName,
195 @NonNull IJsonGenerationState state) {
196
197 Stream<? extends IFlagInstance> flagStream = definition.getFlagInstances().stream();
198
199
200 if (jsonKeyFlagName != null) {
201 IFlagInstance jsonKeyFlag = definition.getFlagInstanceByName(jsonKeyFlagName.getIndexPosition());
202 if (jsonKeyFlag == null) {
203 throw new IllegalArgumentException(
204 String.format("The referenced json-key flag-name '%s' does not exist on definition '%s'.",
205 jsonKeyFlagName,
206 definition.getName()));
207 }
208 flagStream = flagStream.filter(instance -> !jsonKeyFlag.equals(instance));
209 }
210
211 return ObjectUtils.notNull(flagStream
212 .map(instance -> state.getJsonSchemaPropertyFlag(ObjectUtils.requireNonNull(instance)))
213 .collect(Collectors.toUnmodifiableList()));
214 }
215
216 @NonNull
217 public static List<IJsonSchemaPropertyNamed> buildModelProperties(
218 @NonNull IContainerModelAbsolute definition,
219 @NonNull IJsonGenerationState state) {
220 return ObjectUtils.notNull(definition.getModelInstances().stream()
221
222 .filter(instance -> !(instance instanceof IChoiceInstance))
223 .map(instance -> state.getJsonSchemaPropertyModel(ObjectUtils.notNull(instance)))
224 .collect(Collectors.toUnmodifiableList()));
225 }
226
227 public static void generateFieldBody(
228 @NonNull IJsonSchemaDefinitionField field,
229 @NonNull ObjectNode node,
230 @NonNull IJsonGenerationState state) {
231 if (field.getNonValueProperties().isEmpty()) {
232
233 field.getFieldValue().generateJsonSchemaOrDefinitionRef(node, state);
234 } else {
235 generateComplexFieldBody(field, node, state);
236 }
237 }
238
239 private static void generateComplexFieldBody(
240 @NonNull IJsonSchemaDefinitionField field,
241 @NonNull ObjectNode node,
242 @NonNull IJsonGenerationState state) {
243 List<? extends IJsonSchemaPropertyNamed> nonValueProperties = field.getNonValueProperties();
244 node.put("type", "object");
245
246 IFlagInstance jsonValueKeyFlag = field.getDefinition().getJsonValueKeyFlagInstance();
247
248 Stream<? extends IJsonSchemaPropertyNamed> propertiesStream = nonValueProperties.stream();
249
250 if (jsonValueKeyFlag == null) {
251
252 propertiesStream = Stream.concat(propertiesStream, Stream.of(new FieldValueProperty(field)));
253 } else {
254 propertiesStream = propertiesStream.filter(property -> !(property instanceof IJsonSchemaPropertyFlag)
255 || !jsonValueKeyFlag.equals(((IJsonSchemaPropertyFlag) property).getInstance()));
256 }
257
258 List<IJsonSchemaPropertyNamed> properties = ObjectUtils.notNull(propertiesStream
259 .sorted(INSTANCE_NAME_COMPARATOR)
260 .collect(Collectors.toUnmodifiableList()));
261 generateProperties(
262 properties,
263 node,
264 state);
265
266 if (jsonValueKeyFlag == null) {
267 node.put("additionalProperties", false);
268 } else {
269 ObjectNode additionalPropertiesTypeNode;
270
271 additionalPropertiesTypeNode = ObjectUtils.notNull(JsonNodeFactory.instance.objectNode());
272
273 field.getFieldValue().generateJsonSchemaOrDefinitionRef(additionalPropertiesTypeNode, state);
274
275 ObjectNode additionalPropertiesNode = ObjectUtils.notNull(JsonNodeFactory.instance.objectNode());
276 ArrayNode allOf = additionalPropertiesNode.putArray("allOf");
277 allOf.add(additionalPropertiesTypeNode);
278 allOf.addObject()
279 .put(
280 "minProperties",
281
282 properties.stream()
283 .filter(IJsonSchemaPropertyNamed::isRequired)
284 .count() + 1)
285 .put("maxProperties", properties.size() + 1);
286
287 node.set("additionalProperties", additionalPropertiesNode);
288 }
289 }
290
291 public static void generateAssemblyBody(
292 @NonNull IJsonSchemaDefinitionAssembly assembly,
293 @NonNull ObjectNode node,
294 @NonNull IJsonGenerationState state) {
295 node.put("type", "object");
296
297 List<JsonSchemaHelper.Choice> availableChoices = assembly.getChoices();
298
299 if (availableChoices.size() == 1) {
300 generateProperties(
301 availableChoices.iterator().next().getCombinations(),
302 node,
303 state);
304 node.put("additionalProperties", false);
305 } else if (availableChoices.size() > 1) {
306 ArrayNode oneOf = node.putArray("anyOf");
307 availableChoices.forEach(choice -> {
308 ObjectNode schemaNode = ObjectUtils.notNull(oneOf.addObject());
309
310 generateProperties(
311 choice.getCombinations(),
312 schemaNode,
313 state);
314 schemaNode.put("additionalProperties", false);
315 });
316 }
317 }
318
319 private static class FieldValueProperty implements IJsonSchemaPropertyNamed {
320 private final IJsonSchemaDefinitionField field;
321
322 public FieldValueProperty(IJsonSchemaDefinitionField field) {
323 this.field = field;
324 }
325
326 @Override
327 public String getName() {
328 return field.getDefinition().getEffectiveJsonValueKeyName();
329 }
330
331 @Override
332 public Stream<IJsonSchemaDefinable> collectDefinitions(
333 Set<IJsonSchemaDefinitionAssembly> visited,
334 IJsonGenerationState state) {
335 return ObjectUtils.notNull(Stream.empty());
336 }
337
338 @Override
339 public void generate(ObjectNode node, IJsonGenerationState state) {
340 field.getFieldValue().generateJsonSchemaOrDefinitionRef(ObjectUtils.notNull(node.putObject(getName())), state);
341 }
342
343 @Override
344 public boolean isRequired() {
345 return true;
346 }
347 }
348
349 @NonNull
350 public static Stream<Choice> explodeChoices(
351 @NonNull Choice baseChoice,
352 @NonNull List<? extends IChoiceInstance> choiceInstances,
353 @NonNull IJsonGenerationState state) {
354 Stream<Choice> retval = Stream.of(baseChoice);
355 for (IChoiceInstance choice : choiceInstances) {
356 List<IJsonSchemaPropertyNamed> newChoices = buildModelProperties(ObjectUtils.notNull(choice), state);
357 retval = retval.flatMap(oldChoice -> oldChoice.explode(newChoices));
358 }
359 return ObjectUtils.notNull(retval);
360 }
361
362 public static final class Choice {
363 @NonNull
364 private final List<IJsonSchemaPropertyNamed> combinations;
365
366 public Choice(@NonNull List<IJsonSchemaPropertyNamed> combinations) {
367 this.combinations = combinations;
368 }
369
370 @NonNull
371 public List<IJsonSchemaPropertyNamed> getCombinations() {
372 return combinations;
373 }
374
375 @NonNull
376 public Stream<Choice> explode(@NonNull List<IJsonSchemaPropertyNamed> newChoices) {
377 return ObjectUtils.notNull(newChoices.isEmpty()
378 ? Stream.of(this)
379 : newChoices.stream()
380 .map(next -> {
381 List<IJsonSchemaPropertyNamed> retval = new ArrayList<>(combinations.size() + 1);
382 retval.addAll(combinations);
383 retval.add(next);
384 return new Choice(CollectionUtil.unmodifiableList(retval));
385 }));
386 }
387 }
388
389 private JsonSchemaHelper() {
390
391 }
392 }