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.IBindingContext; 015import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader; 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.Writer; 029import java.nio.charset.StandardCharsets; 030import java.nio.file.Files; 031import java.nio.file.Path; 032import java.nio.file.StandardOpenOption; 033import java.util.EnumSet; 034import java.util.List; 035import java.util.Locale; 036import java.util.Set; 037 038import edu.umd.cs.findbugs.annotations.NonNull; 039 040/** 041 * Goal which generates XML and JSON schemas for a given set of Metaschema 042 * modules. 043 */ 044@Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES) 045public class GenerateSchemaMojo 046 extends AbstractMetaschemaMojo { 047 public enum SchemaFormat { 048 XSD, 049 JSON_SCHEMA; 050 } 051 052 @NonNull 053 private static final String STALE_FILE_NAME = "generateSschemaStaleFile"; 054 055 @NonNull 056 private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator(); 057 @NonNull 058 private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator(); 059 060 /** 061 * Specifies the formats of the schemas to generate. Multiple formats can be 062 * supplied and this plugin will generate a schema for each of the desired 063 * formats. 064 * <p> 065 * A format is specified by supplying one of the following values in a 066 * <format> subelement: 067 * <ul> 068 * <li><em>json</em> - Creates a JSON Schema</li> 069 * <li><em>xsd</em> - Creates an XML Schema Definition</li> 070 * </ul> 071 */ 072 @Parameter 073 private List<String> formats; 074 075 /** 076 * If enabled, definitions that are defined inline will be generated as inline 077 * types. If disabled, definitions will always be generated as global types. 078 */ 079 @Parameter(defaultValue = "true") 080 @SuppressWarnings("PMD.ImmutableField") 081 private boolean inlineDefinitions = true; 082 083 /** 084 * If enabled, child definitions of a choice that are defined inline will be 085 * generated as inline types. If disabled, child definitions of a choice will 086 * always be generated as global types. This option will only be used if 087 * <code>inlineDefinitions</code> is also enabled. 088 */ 089 @Parameter(defaultValue = "false") 090 private boolean inlineChoiceDefinitions; // false; 091 092 /** 093 * Determine if inlining definitions is required. 094 * 095 * @return {@code true} if inlining definitions is required, or {@code false} 096 * otherwise 097 */ 098 protected boolean isInlineDefinitions() { 099 return inlineDefinitions; 100 } 101 102 /** 103 * Determine if inlining choice definitions is required. 104 * 105 * @return {@code true} if inlining choice definitions is required, or 106 * {@code false} otherwise 107 */ 108 protected boolean isInlineChoiceDefinitions() { 109 return inlineChoiceDefinitions; 110 } 111 112 /** 113 * <p> 114 * Gets the last part of the stale filename. 115 * </p> 116 * <p> 117 * The full stale filename will be generated by pre-pending 118 * {@code "." + getExecution().getExecutionId()} to this staleFileName. 119 * 120 * @return the stale filename postfix 121 */ 122 @Override 123 protected String getStaleFileName() { 124 return STALE_FILE_NAME; 125 } 126 127 /** 128 * Performs schema generation using the provided Metaschema modules. 129 * 130 * @param modules 131 * the Metaschema modules to generate the schema for 132 * @throws MojoExecutionException 133 * if an error occurred during generation 134 */ 135 protected void generate(@NonNull Set<IModule> modules) throws MojoExecutionException { 136 IMutableConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig 137 = new DefaultConfiguration<>(); 138 139 if (isInlineDefinitions()) { 140 schemaGenerationConfig.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS); 141 } else { 142 schemaGenerationConfig.disableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS); 143 } 144 145 if (isInlineChoiceDefinitions()) { 146 schemaGenerationConfig.enableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS); 147 } else { 148 schemaGenerationConfig.disableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS); 149 } 150 151 Set<SchemaFormat> schemaFormats; 152 if (formats != null) { 153 schemaFormats = ObjectUtils.notNull(EnumSet.noneOf(SchemaFormat.class)); 154 for (String format : formats) { 155 switch (format.toLowerCase(Locale.ROOT)) { 156 case "xsd": 157 schemaFormats.add(SchemaFormat.XSD); 158 break; 159 case "json": 160 schemaFormats.add(SchemaFormat.JSON_SCHEMA); 161 break; 162 default: 163 throw new IllegalStateException("Unsupported schema format: " + format); 164 } 165 } 166 } else { 167 schemaFormats = ObjectUtils.notNull(EnumSet.allOf(SchemaFormat.class)); 168 } 169 170 Path outputDirectory = ObjectUtils.notNull(getOutputDirectory().toPath()); 171 for (IModule module : modules) { 172 if (getLog().isInfoEnabled()) { 173 getLog().info(String.format("Processing metaschema: %s", module.getLocation())); 174 } 175 if (module.getExportedRootAssemblyDefinitions().isEmpty()) { 176 continue; 177 } 178 generateSchemas(module, schemaGenerationConfig, outputDirectory, schemaFormats); 179 } 180 } 181 182 @SuppressWarnings("PMD.AvoidCatchingGenericException") 183 private static void generateSchemas( 184 @NonNull IModule module, 185 @NonNull IConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig, 186 @NonNull Path outputDirectory, 187 @NonNull Set<SchemaFormat> schemaFormats) throws MojoExecutionException { 188 189 String shortName = module.getShortName(); 190 191 if (schemaFormats.contains(SchemaFormat.XSD)) { 192 try { // XML Schema 193 String filename = String.format("%s_schema.xsd", shortName); 194 Path xmlSchema = ObjectUtils.notNull(outputDirectory.resolve(filename)); 195 generateSchema(module, schemaGenerationConfig, xmlSchema, XML_SCHEMA_GENERATOR); 196 } catch (Exception ex) { 197 throw new MojoExecutionException("Unable to generate XML schema.", ex); 198 } 199 } 200 201 if (schemaFormats.contains(SchemaFormat.JSON_SCHEMA)) { 202 try { // JSON Schema 203 String filename = String.format("%s_schema.json", shortName); 204 Path xmlSchema = ObjectUtils.notNull(outputDirectory.resolve(filename)); 205 generateSchema(module, schemaGenerationConfig, xmlSchema, JSON_SCHEMA_GENERATOR); 206 } catch (Exception ex) { 207 throw new MojoExecutionException("Unable to generate JSON schema.", ex); 208 } 209 } 210 } 211 212 private static void generateSchema( 213 @NonNull IModule module, 214 @NonNull IConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig, 215 @NonNull Path schemaPath, 216 @NonNull ISchemaGenerator generator) throws IOException { 217 try (@SuppressWarnings("resource") 218 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 = ObjectUtils.notNull(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 performGeneration(); 256 createStaleFile(staleFile); 257 258 // for m2e 259 getBuildContext().refresh(getOutputDirectory()); 260 } 261 // // add generated sources to Maven 262 // try { 263 // getMavenProject()..addCompileSourceRoot(getOutputDirectory().getCanonicalFile().getPath()); 264 // } catch (IOException ex) { 265 // throw new MojoExecutionException("Unable to add output directory to maven 266 // sources.", ex); 267 // } 268 } 269 270 @SuppressWarnings("PMD.AvoidCatchingGenericException") 271 private void performGeneration() throws MojoExecutionException { 272 File outputDir = getOutputDirectory(); 273 if (getLog().isDebugEnabled()) { 274 getLog().debug(String.format("Using outputDirectory: %s", outputDir.getPath())); 275 } 276 277 if (!outputDir.exists() && !outputDir.mkdirs()) { 278 throw new MojoExecutionException("Unable to create output directory: " + outputDir); 279 } 280 281 IBindingContext bindingContext; 282 try { 283 bindingContext = newBindingContext(); 284 } catch (MetaschemaException | IOException ex) { 285 throw new MojoExecutionException("Failed to create the binding context", ex); 286 } 287 288 IBindingModuleLoader loader = bindingContext.newModuleLoader(); 289 loader.allowEntityResolution(); 290 291 // generate schemas based on provided metaschema sources 292 Set<IModule> modules; 293 try { 294 modules = getModulesToGenerateFor(bindingContext); 295 } catch (Exception ex) { 296 throw new MojoExecutionException("Loading of metaschema modules failed", ex); 297 } 298 299 generate(modules); 300 301 } 302}