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