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