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