001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.schemagen.json;
007
008import com.fasterxml.jackson.core.JsonFactory;
009import com.fasterxml.jackson.core.JsonGenerator;
010import com.fasterxml.jackson.core.JsonGenerator.Feature;
011import com.fasterxml.jackson.databind.ObjectMapper;
012import com.fasterxml.jackson.databind.node.JsonNodeFactory;
013import com.fasterxml.jackson.databind.node.ObjectNode;
014
015import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
016import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
017import gov.nist.secauto.metaschema.core.model.IModule;
018import gov.nist.secauto.metaschema.core.util.ObjectUtils;
019import gov.nist.secauto.metaschema.schemagen.AbstractSchemaGenerator;
020import gov.nist.secauto.metaschema.schemagen.SchemaGenerationException;
021import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
022import gov.nist.secauto.metaschema.schemagen.json.IDefineableJsonSchema.IKey;
023import gov.nist.secauto.metaschema.schemagen.json.impl.JsonDatatypeManager;
024import gov.nist.secauto.metaschema.schemagen.json.impl.JsonGenerationState;
025
026import java.io.IOException;
027import java.io.Writer;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.stream.Collectors;
032
033import edu.umd.cs.findbugs.annotations.NonNull;
034
035public class JsonSchemaGenerator
036    extends AbstractSchemaGenerator<JsonGenerator, JsonDatatypeManager, JsonGenerationState> {
037  @NonNull
038  private final JsonFactory jsonFactory;
039
040  public JsonSchemaGenerator() {
041    this(new JsonFactory());
042  }
043
044  public JsonSchemaGenerator(@NonNull JsonFactory jsonFactory) {
045    this.jsonFactory = jsonFactory;
046  }
047
048  @NonNull
049  public JsonFactory getJsonFactory() {
050    return jsonFactory;
051  }
052
053  @SuppressWarnings("resource")
054  @Override
055  protected JsonGenerator newWriter(Writer out) {
056    try {
057      return ObjectUtils.notNull(getJsonFactory().createGenerator(out)
058          .setCodec(new ObjectMapper())
059          .useDefaultPrettyPrinter()
060          .disable(Feature.AUTO_CLOSE_TARGET));
061    } catch (IOException ex) {
062      throw new SchemaGenerationException(ex);
063    }
064  }
065
066  @Override
067  protected JsonGenerationState newGenerationState(
068      IModule module,
069      JsonGenerator schemaWriter,
070      IConfiguration<SchemaGenerationFeature<?>> configuration) {
071    return new JsonGenerationState(module, schemaWriter, configuration);
072  }
073
074  @Override
075  protected void generateSchema(JsonGenerationState state) {
076    IModule module = state.getModule();
077    try {
078      state.writeStartObject();
079
080      state.writeField("$schema", "http://json-schema.org/draft-07/schema#");
081      state.writeField("$id",
082          String.format("%s/%s-%s-schema.json",
083              module.getXmlNamespace(),
084              module.getShortName(),
085              module.getVersion()));
086      state.writeField("$comment", module.getName().toMarkdown());
087      state.writeField("type", "object");
088
089      ObjectNode definitionsObject = state.generateDefinitions();
090      if (!definitionsObject.isEmpty()) {
091        state.writeField("definitions", definitionsObject);
092      }
093
094      List<IAssemblyDefinition> rootAssemblyDefinitions = state.getMetaschemaIndex().getDefinitions().stream()
095          .map(entry -> entry.getDefinition())
096          .filter(
097              definition -> definition instanceof IAssemblyDefinition && ((IAssemblyDefinition) definition).isRoot())
098          .map(definition -> (IAssemblyDefinition) definition)
099          .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}