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.model.metaschema.BindingModuleLoader;
015import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
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.OutputStream;
029import java.io.Writer;
030import java.nio.charset.StandardCharsets;
031import java.nio.file.Files;
032import java.nio.file.Path;
033import java.nio.file.StandardOpenOption;
034import java.util.EnumSet;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Locale;
038import java.util.Set;
039import java.util.stream.Collectors;
040
041import edu.umd.cs.findbugs.annotations.NonNull;
042
043/**
044 * Goal which generates XML and JSON schemas for a given set of Metaschema
045 * modules.
046 */
047@Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
048public class GenerateSchemaMojo
049    extends AbstractMetaschemaMojo {
050  public enum SchemaFormat {
051    XSD,
052    JSON_SCHEMA;
053  }
054
055  @NonNull
056  private static final String STALE_FILE_NAME = "generateSschemaStaleFile";
057
058  @NonNull
059  private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator();
060  @NonNull
061  private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator();
062
063  /**
064   * Specifies the formats of the schemas to generate. Multiple formats can be
065   * supplied and this plugin will generate a schema for each of the desired
066   * formats.
067   * <p>
068   * A format is specified by supplying one of the following values in a
069   * &lt;format&gt; subelement:
070   * <ul>
071   * <li><em>json</em> - Creates a JSON Schema</li>
072   * <li><em>xsd</em> - Creates an XML Schema Definition</li>
073   * </ul>
074   */
075  @Parameter
076  private List<String> formats;
077
078  /**
079   * If enabled, definitions that are defined inline will be generated as inline
080   * types. If disabled, definitions will always be generated as global types.
081   */
082  @Parameter(defaultValue = "true")
083  private boolean inlineDefinitions = true;
084
085  /**
086   * If enabled, child definitions of a choice that are defined inline will be
087   * generated as inline types. If disabled, child definitions of a choice will
088   * always be generated as global types. This option will only be used if
089   * <code>inlineDefinitions</code> is also enabled.
090   */
091  @Parameter(defaultValue = "false")
092  private boolean inlineChoiceDefinitions; // false;
093
094  /**
095   * Determine if inlining definitions is required.
096   *
097   * @return {@code true} if inlining definitions is required, or {@code false}
098   *         otherwise
099   */
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}