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}