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