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