1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
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     * Supports comparison of named properties by their property name.
53     */
54    public static final Comparator<IJsonSchemaPropertyNamed> INSTANCE_NAME_COMPARATOR
55        = Comparator.comparing(IJsonSchemaPropertyNamed::getName);
56    /**
57     * Supports comparison of JSON schema definitions by their definition name.
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; // use default conversion by default
110     switch (adapter.getJsonRawType()) {
111     case BOOLEAN:
112       if (defaultValue instanceof Boolean) {
113         retval = BooleanNode.valueOf((Boolean) defaultValue);
114       } // else use default conversion
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       } // else use default conversion
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       } // else use default conversion
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       // use default conversion
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    * Generate a JSON pointer expression that points to the provided JSON schema
150    * definition for use as a schema reference.
151    *
152    * @param schema
153    *          the JSON schema definition to generate the pointer for
154    * @return the JSON pointer
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     // determine the flag instances to generate
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         // filter out choice instances, which will be handled separately
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       // simple case
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       // make room for the value
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       // the type of the additional properties must be the datatype of the field value
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               // increment by one to allow for the value key
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     // disable construction
397   }
398 }