001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.maven.plugin;
007
008import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
009import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
010import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
011import gov.nist.secauto.metaschema.core.model.IModule;
012import gov.nist.secauto.metaschema.core.model.MetaschemaException;
013import gov.nist.secauto.metaschema.core.util.ObjectUtils;
014import gov.nist.secauto.metaschema.databind.IBindingContext;
015import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
016import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator;
017import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
018import gov.nist.secauto.metaschema.schemagen.json.JsonSchemaGenerator;
019import gov.nist.secauto.metaschema.schemagen.xml.XmlSchemaGenerator;
020
021import org.apache.maven.plugin.MojoExecutionException;
022import org.apache.maven.plugins.annotations.LifecyclePhase;
023import org.apache.maven.plugins.annotations.Mojo;
024import org.apache.maven.plugins.annotations.Parameter;
025
026import java.io.File;
027import java.io.IOException;
028import java.io.Writer;
029import java.nio.charset.StandardCharsets;
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.nio.file.StandardOpenOption;
033import java.util.EnumSet;
034import java.util.List;
035import java.util.Locale;
036import java.util.Set;
037
038import edu.umd.cs.findbugs.annotations.NonNull;
039
040/**
041 * Goal which generates XML and JSON schemas for a given set of Metaschema
042 * modules.
043 */
044@Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
045public class GenerateSchemaMojo
046    extends AbstractMetaschemaMojo {
047  public enum SchemaFormat {
048    XSD,
049    JSON_SCHEMA;
050  }
051
052  @NonNull
053  private static final String STALE_FILE_NAME = "generateSschemaStaleFile";
054
055  @NonNull
056  private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator();
057  @NonNull
058  private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator();
059
060  /**
061   * Specifies the formats of the schemas to generate. Multiple formats can be
062   * supplied and this plugin will generate a schema for each of the desired
063   * formats.
064   * <p>
065   * A format is specified by supplying one of the following values in a
066   * &lt;format&gt; subelement:
067   * <ul>
068   * <li><em>json</em> - Creates a JSON Schema</li>
069   * <li><em>xsd</em> - Creates an XML Schema Definition</li>
070   * </ul>
071   */
072  @Parameter
073  private List<String> formats;
074
075  /**
076   * If enabled, definitions that are defined inline will be generated as inline
077   * types. If disabled, definitions will always be generated as global types.
078   */
079  @Parameter(defaultValue = "true")
080  @SuppressWarnings("PMD.ImmutableField")
081  private boolean inlineDefinitions = true;
082
083  /**
084   * If enabled, child definitions of a choice that are defined inline will be
085   * generated as inline types. If disabled, child definitions of a choice will
086   * always be generated as global types. This option will only be used if
087   * <code>inlineDefinitions</code> is also enabled.
088   */
089  @Parameter(defaultValue = "false")
090  private boolean inlineChoiceDefinitions; // false;
091
092  /**
093   * Determine if inlining definitions is required.
094   *
095   * @return {@code true} if inlining definitions is required, or {@code false}
096   *         otherwise
097   */
098  protected boolean isInlineDefinitions() {
099    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}