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}