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.model.metaschema.BindingModuleLoader;
15  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
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.OutputStream;
29  import java.io.Writer;
30  import java.nio.charset.StandardCharsets;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.nio.file.StandardOpenOption;
34  import java.util.EnumSet;
35  import java.util.HashSet;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.Set;
39  import java.util.stream.Collectors;
40  
41  import edu.umd.cs.findbugs.annotations.NonNull;
42  
43  /**
44   * Goal which generates XML and JSON schemas for a given set of Metaschema
45   * modules.
46   */
47  @Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
48  public class GenerateSchemaMojo
49      extends AbstractMetaschemaMojo {
50    public enum SchemaFormat {
51      XSD,
52      JSON_SCHEMA;
53    }
54  
55    @NonNull
56    private static final String STALE_FILE_NAME = "generateSschemaStaleFile";
57  
58    @NonNull
59    private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator();
60    @NonNull
61    private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator();
62  
63    /**
64     * Specifies the formats of the schemas to generate. Multiple formats can be
65     * supplied and this plugin will generate a schema for each of the desired
66     * formats.
67     * <p>
68     * A format is specified by supplying one of the following values in a
69     * &lt;format&gt; subelement:
70     * <ul>
71     * <li><em>json</em> - Creates a JSON Schema</li>
72     * <li><em>xsd</em> - Creates an XML Schema Definition</li>
73     * </ul>
74     */
75    @Parameter
76    private List<String> formats;
77  
78    /**
79     * If enabled, definitions that are defined inline will be generated as inline
80     * types. If disabled, definitions will always be generated as global types.
81     */
82    @Parameter(defaultValue = "true")
83    private boolean inlineDefinitions = true;
84  
85    /**
86     * If enabled, child definitions of a choice that are defined inline will be
87     * generated as inline types. If disabled, child definitions of a choice will
88     * always be generated as global types. This option will only be used if
89     * <code>inlineDefinitions</code> is also enabled.
90     */
91    @Parameter(defaultValue = "false")
92    private boolean inlineChoiceDefinitions; // false;
93  
94    /**
95     * Determine if inlining definitions is required.
96     *
97     * @return {@code true} if inlining definitions is required, or {@code false}
98     *         otherwise
99     */
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 }