001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.maven.plugin;
028
029import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
030import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
031import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
032import gov.nist.secauto.metaschema.core.model.IModule;
033import gov.nist.secauto.metaschema.core.model.MetaschemaException;
034import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader;
035import gov.nist.secauto.metaschema.core.util.ObjectUtils;
036import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator;
037import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
038import gov.nist.secauto.metaschema.schemagen.json.JsonSchemaGenerator;
039import gov.nist.secauto.metaschema.schemagen.xml.XmlSchemaGenerator;
040
041import org.apache.maven.plugin.MojoExecutionException;
042import org.apache.maven.plugins.annotations.LifecyclePhase;
043import org.apache.maven.plugins.annotations.Mojo;
044import org.apache.maven.plugins.annotations.Parameter;
045
046import java.io.File;
047import java.io.IOException;
048import java.io.OutputStream;
049import java.io.Writer;
050import java.nio.charset.StandardCharsets;
051import java.nio.file.Files;
052import java.nio.file.Path;
053import java.nio.file.StandardOpenOption;
054import java.util.EnumSet;
055import java.util.HashSet;
056import java.util.List;
057import java.util.Locale;
058import java.util.Set;
059import java.util.stream.Collectors;
060
061import edu.umd.cs.findbugs.annotations.NonNull;
062
063/**
064 * Goal which generates Java source files for a given set of Module definitions.
065 */
066@Mojo(name = "generate-schemas", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
067public class GenerateSchemaMojo
068    extends AbstractMetaschemaMojo {
069  public enum SchemaFormat {
070    XSD,
071    JSON_SCHEMA;
072  }
073
074  @NonNull
075  private static final String STALE_FILE_NAME = "generateSschemaStaleFile";
076
077  @NonNull
078  private static final XmlSchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator();
079  @NonNull
080  private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator();
081
082  /**
083   * Specifies the formats of the schemas to generate. Multiple formats can be
084   * supplied and this plugin will generate a schema for each of the desired
085   * formats.
086   * <p>
087   * A format is specified by supplying one of the following values in a
088   * &lt;format&gt; subelement:
089   * <ul>
090   * <li><em>json</em> - Creates a JSON Schema</li>
091   * <li><em>xsd</em> - Creates an XML Schema Definition</li>
092   * </ul>
093   */
094  @Parameter
095  private List<String> formats;
096
097  /**
098   * If enabled, definitions that are defined inline will be generated as inline
099   * 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}