1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package gov.nist.secauto.metaschema.schemagen.json;
28
29 import com.fasterxml.jackson.core.JsonFactory;
30 import com.fasterxml.jackson.core.JsonGenerator;
31 import com.fasterxml.jackson.core.JsonGenerator.Feature;
32 import com.fasterxml.jackson.databind.ObjectMapper;
33 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
34 import com.fasterxml.jackson.databind.node.ObjectNode;
35
36 import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
37 import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
38 import gov.nist.secauto.metaschema.core.model.IModule;
39 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
40 import gov.nist.secauto.metaschema.schemagen.AbstractSchemaGenerator;
41 import gov.nist.secauto.metaschema.schemagen.SchemaGenerationException;
42 import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
43 import gov.nist.secauto.metaschema.schemagen.json.datatype.JsonDatatypeManager;
44 import gov.nist.secauto.metaschema.schemagen.json.impl.JsonGenerationState;
45
46 import java.io.IOException;
47 import java.io.Writer;
48 import java.util.LinkedHashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.stream.Collectors;
52
53 import edu.umd.cs.findbugs.annotations.NonNull;
54
55 public class JsonSchemaGenerator
56 extends AbstractSchemaGenerator<JsonGenerator, JsonDatatypeManager, JsonGenerationState> {
57 @NonNull
58 private final JsonFactory jsonFactory;
59
60 public JsonSchemaGenerator() {
61 this(new JsonFactory());
62 }
63
64 public JsonSchemaGenerator(@NonNull JsonFactory jsonFactory) {
65 this.jsonFactory = jsonFactory;
66 }
67
68 @NonNull
69 public JsonFactory getJsonFactory() {
70 return jsonFactory;
71 }
72
73 @SuppressWarnings("resource")
74 @Override
75 protected JsonGenerator newWriter(Writer out) {
76 try {
77 return ObjectUtils.notNull(getJsonFactory().createGenerator(out)
78 .setCodec(new ObjectMapper())
79 .useDefaultPrettyPrinter()
80 .disable(Feature.AUTO_CLOSE_TARGET));
81 } catch (IOException ex) {
82 throw new SchemaGenerationException(ex);
83 }
84 }
85
86 @Override
87 protected JsonGenerationState newGenerationState(
88 IModule module,
89 JsonGenerator schemaWriter,
90 IConfiguration<SchemaGenerationFeature<?>> configuration) {
91 return new JsonGenerationState(module, schemaWriter, configuration);
92 }
93
94 @Override
95 protected void generateSchema(JsonGenerationState state) {
96
97 List<IAssemblyDefinition> rootAssemblyDefinitions = analyzeDefinitions(
98 state,
99 (entry, definition) -> {
100 assert entry != null;
101 assert definition != null;
102
103 if (entry.isReferenced()) {
104
105 state.getSchema(definition);
106 }
107 });
108
109 if (rootAssemblyDefinitions.isEmpty()) {
110 throw new SchemaGenerationException("No root definitions found");
111 }
112
113
114 List<RootPropertyEntry> rootEntries = rootAssemblyDefinitions.stream()
115 .map(root -> {
116 assert root != null;
117 return new RootPropertyEntry(root, state);
118 })
119 .collect(Collectors.toUnmodifiableList());
120
121 IModule module = state.getModule();
122 try {
123 state.writeStartObject();
124
125 state.writeField("$schema", "http://json-schema.org/draft-07/schema#");
126 state.writeField("$id",
127 String.format("%s/%s-%s-schema.json",
128 module.getXmlNamespace(),
129 module.getShortName(),
130 module.getVersion()));
131 state.writeField("$comment", module.getName().toMarkdown());
132 state.writeField("type", "object");
133
134 ObjectNode definitionsObject = state.generateDefinitions();
135 if (!definitionsObject.isEmpty()) {
136 state.writeField("definitions", definitionsObject);
137 }
138
139 @SuppressWarnings("resource") JsonGenerator writer = state.getWriter();
140
141 if (rootEntries.size() == 1) {
142 rootEntries.iterator().next().write(writer);
143 } else {
144 writer.writeFieldName("oneOf");
145 writer.writeStartArray();
146
147 for (RootPropertyEntry root : rootEntries) {
148 assert root != null;
149 writer.writeStartObject();
150 root.write(writer);
151 writer.writeEndObject();
152 }
153
154 writer.writeEndArray();
155 }
156 state.writeEndObject();
157 } catch (IOException ex) {
158 throw new SchemaGenerationException(ex);
159 }
160 }
161
162 @NonNull
163 private static Map<String, ObjectNode> generateRootProperties(
164 @NonNull IAssemblyDefinition definition,
165 @NonNull JsonGenerationState state) {
166 Map<String, ObjectNode> properties = new LinkedHashMap<>();
167
168 properties.put("$schema", JsonNodeFactory.instance.objectNode()
169 .put("type", "string")
170 .put("format", "uri-reference"));
171
172 ObjectNode rootObj = ObjectUtils.notNull(JsonNodeFactory.instance.objectNode());
173 state.getSchema(definition).generateSchemaOrRef(state, rootObj);
174
175 properties.put(definition.getRootJsonName(), rootObj);
176 return properties;
177 }
178
179 private static class RootPropertyEntry {
180 @NonNull
181 private final IAssemblyDefinition definition;
182 @NonNull
183 private final Map<String, ObjectNode> properties;
184
185 public RootPropertyEntry(
186 @NonNull IAssemblyDefinition definition,
187 @NonNull JsonGenerationState state) {
188 this.definition = definition;
189 this.properties = generateRootProperties(definition, state);
190 }
191
192 @NonNull
193 protected IAssemblyDefinition getDefinition() {
194 return definition;
195 }
196
197 @NonNull
198 protected Map<String, ObjectNode> getProperties() {
199 return properties;
200 }
201
202 public void write(JsonGenerator writer) throws IOException {
203 writer.writeFieldName("properties");
204 writer.writeStartObject();
205
206 for (Map.Entry<String, ObjectNode> entry : getProperties().entrySet()) {
207 writer.writeFieldName(entry.getKey());
208 writer.writeTree(entry.getValue());
209 }
210
211 writer.writeEndObject();
212
213 writer.writeFieldName("required");
214 writer.writeStartArray();
215 writer.writeString(getDefinition().getRootJsonName());
216 writer.writeEndArray();
217
218 writer.writeBooleanField("additionalProperties", false);
219 }
220 }
221 }