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