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.node.ArrayNode;
9   import com.fasterxml.jackson.databind.node.ObjectNode;
10  
11  import gov.nist.secauto.metaschema.core.model.IModule;
12  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
13  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
14  import gov.nist.secauto.metaschema.schemagen.SchemaGenerationException;
15  
16  import java.util.List;
17  import java.util.Set;
18  import java.util.stream.Collectors;
19  import java.util.stream.Stream;
20  
21  import edu.umd.cs.findbugs.annotations.NonNull;
22  
23  /**
24   * Represents a JSON schema for a Metaschema-based module.
25   */
26  public class JsonSchemaModule
27      implements IJsonSchema {
28    private final IModule module;
29    private final List<IJsonSchemaDefinitionAssembly> roots;
30  
31    /**
32     * Construct a new JSON schema definition.
33     *
34     * @param module
35     *          the associated Metaschema-based module
36     * @param state
37     *          the JSON generation state
38     */
39    public JsonSchemaModule(
40        @NonNull IModule module,
41        @NonNull IJsonGenerationState state) {
42      this.module = module;
43      this.roots = module.getExportedRootAssemblyDefinitions().stream()
44          .map(root -> state.getAssemblyDefinition(ObjectUtils.notNull(root), null))
45          .collect(Collectors.toUnmodifiableList());
46    }
47  
48    @Override
49    public boolean isInline(IJsonGenerationState state) {
50      // always
51      return true;
52    }
53  
54    /**
55     * Get the schemas for referenced definitions for model objects and data types
56     * used within this module schema.
57     *
58     * @param state
59     *          the JSON generation state used for context
60     * @return a stream containing the referenced definitions
61     */
62    @NonNull
63    public Stream<IJsonSchemaDefinable> collectDefinitions(@NonNull IJsonGenerationState state) {
64      return ObjectUtils.notNull(roots.stream()
65          .flatMap(root -> root.collectDefinitions(CollectionUtil.emptySet(), state)));
66    }
67  
68    @Override
69    public void generateInlineJsonSchema(ObjectNode node, IJsonGenerationState state) {
70      node.put("$schema", "http://json-schema.org/draft-07/schema#");
71      node.put("$id",
72          String.format("%s/%s-%s-schema.json",
73              module.getXmlNamespace(),
74              module.getShortName(),
75              module.getVersion()));
76      node.put("$comment", module.getName().toMarkdown());
77      node.put("type", "object");
78  
79      if (roots.isEmpty()) {
80        throw new SchemaGenerationException("No root definitions found");
81      }
82  
83      node.set("definitions", generateDefinitions(state));
84  
85      if (roots.size() == 1) {
86        generateRoot(node, ObjectUtils.notNull(roots.iterator().next()), state);
87      } else {
88        ArrayNode oneOfNode = node.putArray("oneOf");
89        roots.forEach(root -> {
90          assert root != null;
91          ObjectNode rootNode = ObjectUtils.notNull(oneOfNode.addObject());
92          assert rootNode != null;
93  
94          generateRoot(rootNode, root, state);
95        });
96      }
97    }
98  
99    /**
100    * Generate the referenced JSON schema definitions used in this JSON schema.
101    *
102    * @param state
103    *          the JSON generation state used for context
104    * @return the definitions JSON schema node
105    */
106   private ObjectNode generateDefinitions(@NonNull IJsonGenerationState state) {
107 
108     // ensure all definitions are recorded
109     Set<IJsonSchemaDefinable> usedDefinitions = ObjectUtils.notNull(collectDefinitions(state)
110         .collect(Collectors.toUnmodifiableSet()));
111 
112     ObjectNode definitionsNode = ObjectUtils.notNull(state.getJsonNodeFactory().objectNode());
113 
114     usedDefinitions.stream()
115         .filter(definition -> !definition.isInline(state))
116         .distinct()
117         .sorted(JsonSchemaHelper.DEFINABLE_NAME_COMPARATOR)
118         .forEach(definition -> {
119           ObjectNode definitionNode = definitionsNode.putObject(definition.getDefinitionName());
120           assert definitionNode != null;
121           definition.generateDefinitionJsonSchema(definitionNode, state);
122         });
123 
124     state.generateDataTypeDefinitions(definitionsNode);
125 
126     return definitionsNode;
127   }
128 
129   private static void generateRoot(
130       @NonNull ObjectNode node,
131       @NonNull IJsonSchemaDefinitionAssembly schema,
132       @NonNull IJsonGenerationState state) {
133     ObjectNode propertiesObj = node.putObject("properties");
134 
135     propertiesObj.putObject("$schema")
136         .put("type", "string")
137         .put("format", "uri-reference");
138 
139     String name = schema.getDefinition().getRootJsonName();
140 
141     schema.generateJsonSchemaOrDefinitionRef(ObjectUtils.notNull(propertiesObj.putObject(name)), state);
142 
143     node.putArray("required")
144         .add(name);
145     node.put("additionalProperties", false);
146   }
147 }