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