001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.schemagen.json;
028
029import com.fasterxml.jackson.core.JsonFactory;
030import com.fasterxml.jackson.core.JsonGenerator;
031import com.fasterxml.jackson.core.JsonGenerator.Feature;
032import com.fasterxml.jackson.databind.ObjectMapper;
033import com.fasterxml.jackson.databind.node.JsonNodeFactory;
034import com.fasterxml.jackson.databind.node.ObjectNode;
035
036import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
037import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
038import gov.nist.secauto.metaschema.core.model.IModule;
039import gov.nist.secauto.metaschema.core.util.ObjectUtils;
040import gov.nist.secauto.metaschema.schemagen.AbstractSchemaGenerator;
041import gov.nist.secauto.metaschema.schemagen.SchemaGenerationException;
042import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
043import gov.nist.secauto.metaschema.schemagen.json.datatype.JsonDatatypeManager;
044import gov.nist.secauto.metaschema.schemagen.json.impl.JsonGenerationState;
045
046import java.io.IOException;
047import java.io.Writer;
048import java.util.LinkedHashMap;
049import java.util.List;
050import java.util.Map;
051import java.util.stream.Collectors;
052
053import edu.umd.cs.findbugs.annotations.NonNull;
054
055public class JsonSchemaGenerator
056    extends AbstractSchemaGenerator<JsonGenerator, JsonDatatypeManager, JsonGenerationState> {
057  @NonNull
058  private final JsonFactory jsonFactory;
059
060  public JsonSchemaGenerator() {
061    this(new JsonFactory());
062  }
063
064  public JsonSchemaGenerator(@NonNull JsonFactory jsonFactory) {
065    this.jsonFactory = jsonFactory;
066  }
067
068  @NonNull
069  public JsonFactory getJsonFactory() {
070    return jsonFactory;
071  }
072
073  @SuppressWarnings("resource")
074  @Override
075  protected JsonGenerator newWriter(Writer out) {
076    try {
077      return ObjectUtils.notNull(getJsonFactory().createGenerator(out)
078          .setCodec(new ObjectMapper())
079          .useDefaultPrettyPrinter()
080          .disable(Feature.AUTO_CLOSE_TARGET));
081    } catch (IOException ex) {
082      throw new SchemaGenerationException(ex);
083    }
084  }
085
086  @Override
087  protected JsonGenerationState newGenerationState(
088      IModule module,
089      JsonGenerator schemaWriter,
090      IConfiguration<SchemaGenerationFeature<?>> configuration) {
091    return new JsonGenerationState(module, schemaWriter, configuration);
092  }
093
094  @Override
095  protected void generateSchema(JsonGenerationState state) {
096    // analyze all definitions
097    List<IAssemblyDefinition> rootAssemblyDefinitions = analyzeDefinitions(
098        state,
099        (entry, definition) -> {
100          assert entry != null;
101          assert definition != null;
102
103          if (entry.isReferenced()) {
104            // ensure schema is generated
105            state.getSchema(definition);
106          }
107        });
108
109    if (rootAssemblyDefinitions.isEmpty()) {
110      throw new SchemaGenerationException("No root definitions found");
111    }
112
113    // generate the properties first to ensure all definitions are identified
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(); // NOPMD not owned
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<>(); // NOPMD no concurrent access
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}