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.util.CollectionUtil;
13  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
14  import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator;
15  import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
16  import gov.nist.secauto.metaschema.schemagen.json.JsonSchemaGenerator;
17  import gov.nist.secauto.metaschema.schemagen.xml.XmlSchemaGenerator;
18  
19  import org.apache.maven.plugin.MojoExecutionException;
20  import org.apache.maven.plugins.annotations.LifecyclePhase;
21  import org.apache.maven.plugins.annotations.Mojo;
22  import org.apache.maven.plugins.annotations.Parameter;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.Writer;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.nio.file.StandardOpenOption;
31  import java.util.EnumSet;
32  import java.util.LinkedList;
33  import java.util.List;
34  import java.util.Locale;
35  import java.util.Set;
36  
37  import edu.umd.cs.findbugs.annotations.NonNull;
38  
39  /**
40   * Goal which generates XML and JSON schemas for a given set of Metaschema
41   * modules.
42   */
43  @Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
44  public class GenerateSchemaMojo
45      extends AbstractMetaschemaMojo {
46    public enum SchemaFormat {
47      XSD,
48      JSON_SCHEMA;
49    }
50  
51    @NonNull
52    private static final String STALE_FILE_NAME = "generateSschemaStaleFile";
53  
54    @NonNull
55    private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator();
56    @NonNull
57    private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator();
58  
59    /**
60     * Specifies the formats of the schemas to generate. Multiple formats can be
61     * supplied and this plugin will generate a schema for each of the desired
62     * formats.
63     * <p>
64     * A format is specified by supplying one of the following values in a
65     * &lt;format&gt; subelement:
66     * <ul>
67     * <li><em>json</em> - Creates a JSON Schema
68     * <li><em>xsd</em> - Creates an XML Schema Definition
69     * </ul>
70     */
71    @Parameter
72    private List<String> formats;
73  
74    /**
75     * If enabled, definitions that are defined inline will be generated as inline
76     * types. If disabled, definitions will always be generated as global types.
77     */
78    @Parameter(defaultValue = "true")
79    @SuppressWarnings("PMD.ImmutableField")
80    private boolean inlineDefinitions = true;
81  
82    /**
83     * If enabled, child definitions of a choice that are defined inline will be
84     * generated as inline types. If disabled, child definitions of a choice will
85     * always be generated as global types. This option will only be used if
86     * <code>inlineDefinitions</code> is also enabled.
87     */
88    @Parameter(defaultValue = "false")
89    private boolean inlineChoiceDefinitions; // false;
90  
91    /**
92     * Determine if inlining definitions is required.
93     *
94     * @return {@code true} if inlining definitions is required, or {@code false}
95     *         otherwise
96     */
97    protected boolean isInlineDefinitions() {
98      return inlineDefinitions;
99    }
100 
101   /**
102    * Determine if inlining choice definitions is required.
103    *
104    * @return {@code true} if inlining choice definitions is required, or
105    *         {@code false} otherwise
106    */
107   protected boolean isInlineChoiceDefinitions() {
108     return inlineChoiceDefinitions;
109   }
110 
111   /**
112    * <p>
113    * Gets the last part of the stale filename.
114    * </p>
115    * <p>
116    * The full stale filename will be generated by pre-pending
117    * {@code "." + getExecution().getExecutionId()} to this staleFileName.
118    *
119    * @return the stale filename postfix
120    */
121   @Override
122   protected String getStaleFileName() {
123     return STALE_FILE_NAME;
124   }
125 
126   /**
127    * Performs schema generation using the provided Metaschema modules.
128    *
129    * @param modules
130    *          the Metaschema modules to generate the schema for
131    * @throws MojoExecutionException
132    *           if an error occurred during generation
133    */
134   @Override
135   @NonNull
136   protected List<File> generate(@NonNull Set<IModule> modules) throws MojoExecutionException {
137     IMutableConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig
138         = new DefaultConfiguration<>();
139 
140     if (isInlineDefinitions()) {
141       schemaGenerationConfig.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS);
142     } else {
143       schemaGenerationConfig.disableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS);
144     }
145 
146     if (isInlineChoiceDefinitions()) {
147       schemaGenerationConfig.enableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS);
148     } else {
149       schemaGenerationConfig.disableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS);
150     }
151 
152     Set<SchemaFormat> schemaFormats;
153     if (formats != null) {
154       schemaFormats = ObjectUtils.notNull(EnumSet.noneOf(SchemaFormat.class));
155       for (String format : formats) {
156         switch (format.toLowerCase(Locale.ROOT)) {
157         case "xsd":
158           schemaFormats.add(SchemaFormat.XSD);
159           break;
160         case "json":
161           schemaFormats.add(SchemaFormat.JSON_SCHEMA);
162           break;
163         default:
164           throw new IllegalStateException("Unsupported schema format: " + format);
165         }
166       }
167     } else {
168       schemaFormats = ObjectUtils.notNull(EnumSet.allOf(SchemaFormat.class));
169     }
170 
171     Path outputDirectory = ObjectUtils.notNull(getOutputDirectory().toPath());
172     List<File> generatedSchemas = new LinkedList<>();
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       generatedSchemas.addAll(generateSchemas(module, schemaGenerationConfig, outputDirectory, schemaFormats));
181     }
182     return CollectionUtil.unmodifiableList(generatedSchemas);
183   }
184 
185   @SuppressWarnings("PMD.AvoidCatchingGenericException")
186   @NonNull
187   private List<File> generateSchemas(
188       @NonNull IModule module,
189       @NonNull IConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig,
190       @NonNull Path outputDirectory,
191       @NonNull Set<SchemaFormat> schemaFormats) throws MojoExecutionException {
192 
193     String shortName = module.getShortName();
194 
195     List<File> generatedSchemas = new LinkedList<>();
196     if (schemaFormats.contains(SchemaFormat.XSD)) {
197       try { // XML Schema
198         String filename = String.format("%s_schema.xsd", shortName);
199         Path xmlSchema = ObjectUtils.notNull(outputDirectory.resolve(filename));
200         generateSchema(module, schemaGenerationConfig, xmlSchema, XML_SCHEMA_GENERATOR);
201         generatedSchemas.add(xmlSchema.toFile());
202       } catch (Exception ex) {
203         throw new MojoExecutionException("Unable to generate XML schema.", ex);
204       }
205     }
206 
207     if (schemaFormats.contains(SchemaFormat.JSON_SCHEMA)) {
208       try { // JSON Schema
209         String filename = String.format("%s_schema.json", shortName);
210         Path jsonSchema = ObjectUtils.notNull(outputDirectory.resolve(filename));
211         generateSchema(module, schemaGenerationConfig, jsonSchema, JSON_SCHEMA_GENERATOR);
212         generatedSchemas.add(jsonSchema.toFile());
213       } catch (Exception ex) {
214         throw new MojoExecutionException("Unable to generate JSON schema.", ex);
215       }
216     }
217     return CollectionUtil.unmodifiableList(generatedSchemas);
218   }
219 
220   private static void generateSchema(
221       @NonNull IModule module,
222       @NonNull IConfiguration<SchemaGenerationFeature<?>> schemaGenerationConfig,
223       @NonNull Path schemaPath,
224       @NonNull ISchemaGenerator generator) throws IOException {
225     try (@SuppressWarnings("resource")
226     Writer writer = ObjectUtils.notNull(Files.newBufferedWriter(
227         schemaPath,
228         StandardCharsets.UTF_8,
229         StandardOpenOption.CREATE,
230         StandardOpenOption.WRITE,
231         StandardOpenOption.TRUNCATE_EXISTING))) {
232       generator.generateFromModule(module, writer, schemaGenerationConfig);
233     }
234   }
235 }