View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.metaschema.maven.plugin;
28  
29  import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
30  import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
31  import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
32  import gov.nist.secauto.metaschema.core.model.IModule;
33  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
34  import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader;
35  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
36  import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator;
37  import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
38  import gov.nist.secauto.metaschema.schemagen.json.JsonSchemaGenerator;
39  import gov.nist.secauto.metaschema.schemagen.xml.XmlSchemaGenerator;
40  
41  import org.apache.maven.plugin.MojoExecutionException;
42  import org.apache.maven.plugins.annotations.LifecyclePhase;
43  import org.apache.maven.plugins.annotations.Mojo;
44  import org.apache.maven.plugins.annotations.Parameter;
45  
46  import java.io.File;
47  import java.io.IOException;
48  import java.io.OutputStream;
49  import java.io.Writer;
50  import java.nio.charset.StandardCharsets;
51  import java.nio.file.Files;
52  import java.nio.file.Path;
53  import java.nio.file.StandardOpenOption;
54  import java.util.EnumSet;
55  import java.util.HashSet;
56  import java.util.List;
57  import java.util.Locale;
58  import java.util.Set;
59  import java.util.stream.Collectors;
60  
61  import edu.umd.cs.findbugs.annotations.NonNull;
62  
63  /**
64   * Goal which generates Java source files for a given set of Module definitions.
65   */
66  @Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
67  public class GenerateSchemaMojo
68      extends AbstractMetaschemaMojo {
69    public enum SchemaFormat {
70      XSD,
71      JSON_SCHEMA;
72    }
73  
74    @NonNull
75    private static final String STALE_FILE_NAME = "generateSschemaStaleFile";
76  
77    @NonNull
78    private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator();
79    @NonNull
80    private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator();
81  
82    /**
83     * Specifies the formats of the schemas to generate. Multiple formats can be
84     * supplied and this plugin will generate a schema for each of the desired
85     * formats.
86     * <p>
87     * A format is specified by supplying one of the following values in a
88     * &lt;format&gt; subelement:
89     * <ul>
90     * <li><em>json</em> - Creates a JSON Schema</li>
91     * <li><em>xsd</em> - Creates an XML Schema Definition</li>
92     * </ul>
93     */
94    @Parameter
95    private List<String> formats;
96  
97    /**
98     * If enabled, definitions that are defined inline will be generated as inline
99     * types. If disabled, definitions will always be generated as global types.
100    */
101   @Parameter(defaultValue = "true")
102   private boolean inlineDefinitions = true;
103 
104   /**
105    * If enabled, child definitions of a choice that are defined inline will be
106    * generated as inline types. If disabled, child definitions of a choice will
107    * always be generated as global types. This option will only be used if
108    * <code>inlineDefinitions</code> is also enabled.
109    */
110   @Parameter(defaultValue = "false")
111   private boolean inlineChoiceDefinitions = false;
112 
113   /**
114    * Determine if inlining definitions is required.
115    *
116    * @return {@code true} if inlining definitions is required, or {@code false}
117    *         otherwise
118    */
119   protected boolean isInlineDefinitions() {
120     return inlineDefinitions;
121   }
122 
123   /**
124    * Determine if inlining choice definitions is required.
125    *
126    * @return {@code true} if inlining choice definitions is required, or
127    *         {@code false} otherwise
128    */
129   protected boolean isInlineChoiceDefinitions() {
130     return inlineChoiceDefinitions;
131   }
132 
133   /**
134    * <p>
135    * Gets the last part of the stale filename.
136    * </p>
137    * <p>
138    * The full stale filename will be generated by pre-pending
139    * {@code "." + getExecution().getExecutionId()} to this staleFileName.
140    *
141    * @return the stale filename postfix
142    */
143   @Override
144   protected String getStaleFileName() {
145     return STALE_FILE_NAME;
146   }
147 
148   /**
149    * Performs schema generation using the provided Metaschema modules.
150    *
151    * @param modules
152    *          the Metaschema modules to generate the schema for
153    * @throws MojoExecutionException
154    *           if an error occurred during generation
155    */
156   protected void generate(@NonNull Set<IModule> modules) throws MojoExecutionException {
157     IMutableConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig
158         = new DefaultConfiguration<>();
159 
160     if (isInlineDefinitions()) {
161       schemaGenerationConfig.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS);
162     } else {
163       schemaGenerationConfig.disableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS);
164     }
165 
166     if (isInlineChoiceDefinitions()) {
167       schemaGenerationConfig.enableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS);
168     } else {
169       schemaGenerationConfig.disableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS);
170     }
171 
172     Set<SchemaFormat> schemaFormats;
173     if (formats != null) {
174       schemaFormats = ObjectUtils.notNull(EnumSet.noneOf(SchemaFormat.class));
175       for (String format : formats) {
176         switch (format.toLowerCase(Locale.ROOT)) {
177         case "xsd":
178           schemaFormats.add(SchemaFormat.XSD);
179           break;
180         case "json":
181           schemaFormats.add(SchemaFormat.JSON_SCHEMA);
182           break;
183         default:
184           throw new IllegalStateException("Unsupported schema format: " + format);
185         }
186       }
187     } else {
188       schemaFormats = ObjectUtils.notNull(EnumSet.allOf(SchemaFormat.class));
189     }
190 
191     Path outputDirectory = ObjectUtils.notNull(getOutputDirectory().toPath());
192     for (IModule module : modules) {
193       getLog().info(String.format("Processing metaschema: %s", module.getLocation()));
194       if (module.getExportedRootAssemblyDefinitions().isEmpty()) {
195         continue;
196       }
197 
198       generateSchemas(module, schemaGenerationConfig, outputDirectory, schemaFormats);
199     }
200   }
201 
202   private static void generateSchemas(
203       @NonNull IModule module,
204       @NonNull IConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig,
205       @NonNull Path outputDirectory,
206       @NonNull Set<SchemaFormat> schemaFormats) throws MojoExecutionException {
207 
208     String shortName = module.getShortName();
209 
210     if (schemaFormats.contains(SchemaFormat.XSD)) {
211       try { // XML Schema
212         String filename = String.format("%s_schema.xsd", shortName);
213         Path xmlSchema = ObjectUtils.notNull(outputDirectory.resolve(filename));
214         generateSchema(module, schemaGenerationConfig, xmlSchema, XML_SCHEMA_GENERATOR);
215       } catch (Exception ex) {
216         throw new MojoExecutionException("Unable to generate XML schema.", ex);
217       }
218     }
219 
220     if (schemaFormats.contains(SchemaFormat.JSON_SCHEMA)) {
221       try { // JSON Schema
222         String filename = String.format("%s_schema.json", shortName);
223         Path xmlSchema = ObjectUtils.notNull(outputDirectory.resolve(filename));
224         generateSchema(module, schemaGenerationConfig, xmlSchema, JSON_SCHEMA_GENERATOR);
225       } catch (Exception ex) {
226         throw new MojoExecutionException("Unable to generate JSON schema.", ex);
227       }
228     }
229   }
230 
231   private static void generateSchema(
232       @NonNull IModule module,
233       @NonNull IConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig,
234       @NonNull Path schemaPath,
235       @NonNull ISchemaGenerator generator) throws IOException {
236     try (@SuppressWarnings("resource") Writer writer = ObjectUtils.notNull(Files.newBufferedWriter(
237         schemaPath,
238         StandardCharsets.UTF_8,
239         StandardOpenOption.CREATE,
240         StandardOpenOption.WRITE,
241         StandardOpenOption.TRUNCATE_EXISTING))) {
242       generator.generateFromModule(module, writer, schemaGenerationConfig);
243     }
244   }
245 
246   @Override
247   public void execute() throws MojoExecutionException {
248     File staleFile = getStaleFile();
249     try {
250       staleFile = staleFile.getCanonicalFile();
251     } catch (IOException ex) {
252       getLog().warn("Unable to resolve canonical path to stale file. Treating it as not existing.", ex);
253     }
254 
255     boolean generate;
256     if (shouldExecutionBeSkipped()) {
257       getLog().debug(String.format("Schema generation is configured to be skipped. Skipping."));
258       generate = false;
259     } else if (!staleFile.exists()) {
260       getLog().info(String.format("Stale file '%s' doesn't exist! Generating source files.", staleFile.getPath()));
261       generate = true;
262     } else {
263       generate = isGenerationRequired();
264     }
265 
266     if (generate) {
267       File outputDir = getOutputDirectory();
268       getLog().debug(String.format("Using outputDirectory: %s", outputDir.getPath()));
269 
270       if (!outputDir.exists()) {
271         if (!outputDir.mkdirs()) {
272           throw new MojoExecutionException("Unable to create output directory: " + outputDir);
273         }
274       }
275 
276       // generate Java sources based on provided Module sources
277       final ModuleLoader loader = new ModuleLoader();
278       loader.allowEntityResolution();
279       final Set<IModule> modules = new HashSet<>();
280       for (File source : getSources().collect(Collectors.toList())) {
281         getLog().info("Using metaschema source: " + source.getPath());
282         IModule module;
283         try {
284           module = loader.load(source);
285         } catch (MetaschemaException | IOException ex) {
286           throw new MojoExecutionException("Loading of metaschema failed", ex);
287         }
288         modules.add(module);
289       }
290 
291       generate(modules);
292 
293       // create the stale file
294       if (!staleFileDirectory.exists()) {
295         if (!staleFileDirectory.mkdirs()) {
296           throw new MojoExecutionException("Unable to create output directory: " + staleFileDirectory);
297         }
298       }
299       try (OutputStream os
300           = Files.newOutputStream(staleFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE,
301               StandardOpenOption.TRUNCATE_EXISTING)) {
302         os.close();
303         getLog().info("Created stale file: " + staleFile);
304       } catch (IOException ex) {
305         throw new MojoExecutionException("Failed to write stale file: " + staleFile.getPath(), ex);
306       }
307 
308       // for m2e
309       getBuildContext().refresh(getOutputDirectory());
310     }
311 
312     // // add generated sources to Maven
313     // try {
314     // getMavenProject()..addCompileSourceRoot(getOutputDirectory().getCanonicalFile().getPath());
315     // } catch (IOException ex) {
316     // throw new MojoExecutionException("Unable to add output directory to maven
317     // sources.", ex);
318     // }
319   }
320 }