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}