001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.maven.plugin; 007 008import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration; 009import gov.nist.secauto.metaschema.core.configuration.IConfiguration; 010import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration; 011import gov.nist.secauto.metaschema.core.model.IModule; 012import gov.nist.secauto.metaschema.core.model.MetaschemaException; 013import gov.nist.secauto.metaschema.core.util.ObjectUtils; 014import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader; 015import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule; 016import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator; 017import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature; 018import gov.nist.secauto.metaschema.schemagen.json.JsonSchemaGenerator; 019import gov.nist.secauto.metaschema.schemagen.xml.XmlSchemaGenerator; 020 021import org.apache.maven.plugin.MojoExecutionException; 022import org.apache.maven.plugins.annotations.LifecyclePhase; 023import org.apache.maven.plugins.annotations.Mojo; 024import org.apache.maven.plugins.annotations.Parameter; 025 026import java.io.File; 027import java.io.IOException; 028import java.io.OutputStream; 029import java.io.Writer; 030import java.nio.charset.StandardCharsets; 031import java.nio.file.Files; 032import java.nio.file.Path; 033import java.nio.file.StandardOpenOption; 034import java.util.EnumSet; 035import java.util.HashSet; 036import java.util.List; 037import java.util.Locale; 038import java.util.Set; 039import java.util.stream.Collectors; 040 041import edu.umd.cs.findbugs.annotations.NonNull; 042 043/** 044 * Goal which generates XML and JSON schemas for a given set of Metaschema 045 * modules. 046 */ 047@Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES) 048public class GenerateSchemaMojo 049 extends AbstractMetaschemaMojo { 050 public enum SchemaFormat { 051 XSD, 052 JSON_SCHEMA; 053 } 054 055 @NonNull 056 private static final String STALE_FILE_NAME = "generateSschemaStaleFile"; 057 058 @NonNull 059 private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator(); 060 @NonNull 061 private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator(); 062 063 /** 064 * Specifies the formats of the schemas to generate. Multiple formats can be 065 * supplied and this plugin will generate a schema for each of the desired 066 * formats. 067 * <p> 068 * A format is specified by supplying one of the following values in a 069 * <format> subelement: 070 * <ul> 071 * <li><em>json</em> - Creates a JSON Schema</li> 072 * <li><em>xsd</em> - Creates an XML Schema Definition</li> 073 * </ul> 074 */ 075 @Parameter 076 private List<String> formats; 077 078 /** 079 * If enabled, definitions that are defined inline will be generated as inline 080 * types. If disabled, definitions will always be generated as global types. 081 */ 082 @Parameter(defaultValue = "true") 083 private boolean inlineDefinitions = true; 084 085 /** 086 * If enabled, child definitions of a choice that are defined inline will be 087 * generated as inline types. If disabled, child definitions of a choice will 088 * always be generated as global types. This option will only be used if 089 * <code>inlineDefinitions</code> is also enabled. 090 */ 091 @Parameter(defaultValue = "false") 092 private boolean inlineChoiceDefinitions; // false; 093 094 /** 095 * Determine if inlining definitions is required. 096 * 097 * @return {@code true} if inlining definitions is required, or {@code false} 098 * otherwise 099 */ 100 protected boolean isInlineDefinitions() { 101 return inlineDefinitions; 102 } 103 104 /** 105 * Determine if inlining choice definitions is required. 106 * 107 * @return {@code true} if inlining choice definitions is required, or 108 * {@code false} otherwise 109 */ 110 protected boolean isInlineChoiceDefinitions() { 111 return inlineChoiceDefinitions; 112 } 113 114 /** 115 * <p> 116 * Gets the last part of the stale filename. 117 * </p> 118 * <p> 119 * The full stale filename will be generated by pre-pending 120 * {@code "." + getExecution().getExecutionId()} to this staleFileName. 121 * 122 * @return the stale filename postfix 123 */ 124 @Override 125 protected String getStaleFileName() { 126 return STALE_FILE_NAME; 127 } 128 129 /** 130 * Performs schema generation using the provided Metaschema modules. 131 * 132 * @param modules 133 * the Metaschema modules to generate the schema for 134 * @throws MojoExecutionException 135 * if an error occurred during generation 136 */ 137 protected void generate(@NonNull Set<IModule> modules) throws MojoExecutionException { 138 IMutableConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig 139 = new DefaultConfiguration<>(); 140 141 if (isInlineDefinitions()) { 142 schemaGenerationConfig.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS); 143 } else { 144 schemaGenerationConfig.disableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS); 145 } 146 147 if (isInlineChoiceDefinitions()) { 148 schemaGenerationConfig.enableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS); 149 } else { 150 schemaGenerationConfig.disableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS); 151 } 152 153 Set<SchemaFormat> schemaFormats; 154 if (formats != null) { 155 schemaFormats = ObjectUtils.notNull(EnumSet.noneOf(SchemaFormat.class)); 156 for (String format : formats) { 157 switch (format.toLowerCase(Locale.ROOT)) { 158 case "xsd": 159 schemaFormats.add(SchemaFormat.XSD); 160 break; 161 case "json": 162 schemaFormats.add(SchemaFormat.JSON_SCHEMA); 163 break; 164 default: 165 throw new IllegalStateException("Unsupported schema format: " + format); 166 } 167 } 168 } else { 169 schemaFormats = ObjectUtils.notNull(EnumSet.allOf(SchemaFormat.class)); 170 } 171 172 Path outputDirectory = ObjectUtils.notNull(getOutputDirectory().toPath()); 173 for (IModule module : modules) { 174 if (getLog().isInfoEnabled()) { 175 getLog().info(String.format("Processing metaschema: %s", module.getLocation())); 176 } 177 if (module.getExportedRootAssemblyDefinitions().isEmpty()) { 178 continue; 179 } 180 generateSchemas(module, schemaGenerationConfig, outputDirectory, schemaFormats); 181 } 182 } 183 184 private static void generateSchemas( 185 @NonNull IModule module, 186 @NonNull IConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig, 187 @NonNull Path outputDirectory, 188 @NonNull Set<SchemaFormat> schemaFormats) throws MojoExecutionException { 189 190 String shortName = module.getShortName(); 191 192 if (schemaFormats.contains(SchemaFormat.XSD)) { 193 try { // XML Schema 194 String filename = String.format("%s_schema.xsd", shortName); 195 Path xmlSchema = ObjectUtils.notNull(outputDirectory.resolve(filename)); 196 generateSchema(module, schemaGenerationConfig, xmlSchema, XML_SCHEMA_GENERATOR); 197 } catch (Exception ex) { 198 throw new MojoExecutionException("Unable to generate XML schema.", ex); 199 } 200 } 201 202 if (schemaFormats.contains(SchemaFormat.JSON_SCHEMA)) { 203 try { // JSON Schema 204 String filename = String.format("%s_schema.json", shortName); 205 Path xmlSchema = ObjectUtils.notNull(outputDirectory.resolve(filename)); 206 generateSchema(module, schemaGenerationConfig, xmlSchema, JSON_SCHEMA_GENERATOR); 207 } catch (Exception ex) { 208 throw new MojoExecutionException("Unable to generate JSON schema.", ex); 209 } 210 } 211 } 212 213 private static void generateSchema( 214 @NonNull IModule module, 215 @NonNull IConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig, 216 @NonNull Path schemaPath, 217 @NonNull ISchemaGenerator generator) throws IOException { 218 try (@SuppressWarnings("resource") Writer writer = ObjectUtils.notNull(Files.newBufferedWriter( 219 schemaPath, 220 StandardCharsets.UTF_8, 221 StandardOpenOption.CREATE, 222 StandardOpenOption.WRITE, 223 StandardOpenOption.TRUNCATE_EXISTING))) { 224 generator.generateFromModule(module, writer, schemaGenerationConfig); 225 } 226 } 227 228 @Override 229 public void execute() throws MojoExecutionException { 230 File staleFile = getStaleFile(); 231 try { 232 staleFile = staleFile.getCanonicalFile(); 233 } catch (IOException ex) { 234 if (getLog().isWarnEnabled()) { 235 getLog().warn("Unable to resolve canonical path to stale file. Treating it as not existing.", ex); 236 } 237 } 238 239 boolean generate; 240 if (shouldExecutionBeSkipped()) { 241 if (getLog().isDebugEnabled()) { 242 getLog().debug(String.format("Schema generation is configured to be skipped. Skipping.")); 243 } 244 generate = false; 245 } else if (staleFile.exists()) { 246 generate = isGenerationRequired(); 247 } else { 248 if (getLog().isInfoEnabled()) { 249 getLog().info(String.format("Stale file '%s' doesn't exist! Generating source files.", staleFile.getPath())); 250 } 251 generate = true; 252 } 253 254 if (generate) { 255 File outputDir = getOutputDirectory(); 256 if (getLog().isDebugEnabled()) { 257 getLog().debug(String.format("Using outputDirectory: %s", outputDir.getPath())); 258 } 259 260 if (!outputDir.exists() && !outputDir.mkdirs()) { 261 throw new MojoExecutionException("Unable to create output directory: " + outputDir); 262 } 263 264 BindingModuleLoader loader = newModuleLoader(); 265 266 // generate Java sources based on provided metaschema sources 267 final Set<IModule> modules = new HashSet<>(); 268 for (File source : getModuleSources().collect(Collectors.toList())) { 269 assert source != null; 270 if (getLog().isInfoEnabled()) { 271 getLog().info("Using metaschema source: " + source.getPath()); 272 } 273 IBindingMetaschemaModule module; 274 try { 275 module = loader.load(source); 276 } catch (MetaschemaException | IOException ex) { 277 throw new MojoExecutionException("Loading of metaschema failed", ex); 278 } 279 280 // IValidationResult result = IBindingContext.instance().validate( 281 // module.getSourceNodeItem(), 282 // IBindingContext.instance().newBoundLoader(), 283 // null); 284 // 285 // LoggingValidationHandler.instance().handleValidationResults(validationResult); 286 287 modules.add(module); 288 } 289 290 generate(modules); 291 292 // create the stale file 293 if (!staleFileDirectory.exists() && !staleFileDirectory.mkdirs()) { 294 throw new MojoExecutionException("Unable to create output directory: " + staleFileDirectory); 295 } 296 try (OutputStream os 297 = Files.newOutputStream(staleFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, 298 StandardOpenOption.TRUNCATE_EXISTING)) { 299 os.close(); 300 if (getLog().isInfoEnabled()) { 301 getLog().info("Created stale file: " + staleFile); 302 } 303 } catch (IOException ex) { 304 throw new MojoExecutionException("Failed to write stale file: " + staleFile.getPath(), ex); 305 } 306 307 // for m2e 308 getBuildContext().refresh(getOutputDirectory()); 309 } 310 311 // // add generated sources to Maven 312 // try { 313 // getMavenProject()..addCompileSourceRoot(getOutputDirectory().getCanonicalFile().getPath()); 314 // } catch (IOException ex) { 315 // throw new MojoExecutionException("Unable to add output directory to maven 316 // sources.", ex); 317 // } 318 } 319}