1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.maven.plugin;
7   
8   import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
9   import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
10  import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
11  import gov.nist.secauto.metaschema.core.model.IModule;
12  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
13  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
14  import gov.nist.secauto.metaschema.databind.IBindingContext;
15  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
16  import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator;
17  import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
18  import gov.nist.secauto.metaschema.schemagen.json.JsonSchemaGenerator;
19  import gov.nist.secauto.metaschema.schemagen.xml.XmlSchemaGenerator;
20  
21  import org.apache.maven.plugin.MojoExecutionException;
22  import org.apache.maven.plugins.annotations.LifecyclePhase;
23  import org.apache.maven.plugins.annotations.Mojo;
24  import org.apache.maven.plugins.annotations.Parameter;
25  
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.Writer;
29  import java.nio.charset.StandardCharsets;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.StandardOpenOption;
33  import java.util.EnumSet;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Set;
37  
38  import edu.umd.cs.findbugs.annotations.NonNull;
39  
40  /**
41   * Goal which generates XML and JSON schemas for a given set of Metaschema
42   * modules.
43   */
44  @Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
45  public class GenerateSchemaMojo
46      extends AbstractMetaschemaMojo {
47    public enum SchemaFormat {
48      XSD,
49      JSON_SCHEMA;
50    }
51  
52    @NonNull
53    private static final String STALE_FILE_NAME = "generateSschemaStaleFile";
54  
55    @NonNull
56    private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator();
57    @NonNull
58    private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator();
59  
60    /**
61     * Specifies the formats of the schemas to generate. Multiple formats can be
62     * supplied and this plugin will generate a schema for each of the desired
63     * formats.
64     * <p>
65     * A format is specified by supplying one of the following values in a
66     * &lt;format&gt; subelement:
67     * <ul>
68     * <li><em>json</em> - Creates a JSON Schema</li>
69     * <li><em>xsd</em> - Creates an XML Schema Definition</li>
70     * </ul>
71     */
72    @Parameter
73    private List<String> formats;
74  
75    /**
76     * If enabled, definitions that are defined inline will be generated as inline
77     * types. If disabled, definitions will always be generated as global types.
78     */
79    @Parameter(defaultValue = "true")
80    @SuppressWarnings("PMD.ImmutableField")
81    private boolean inlineDefinitions = true;
82  
83    /**
84     * If enabled, child definitions of a choice that are defined inline will be
85     * generated as inline types. If disabled, child definitions of a choice will
86     * always be generated as global types. This option will only be used if
87     * <code>inlineDefinitions</code> is also enabled.
88     */
89    @Parameter(defaultValue = "false")
90    private boolean inlineChoiceDefinitions; // false;
91  
92    /**
93     * Determine if inlining definitions is required.
94     *
95     * @return {@code true} if inlining definitions is required, or {@code false}
96     *         otherwise
97     */
98    protected boolean isInlineDefinitions() {
99      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 }