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.model.IConstraintLoader;
009import gov.nist.secauto.metaschema.core.model.MetaschemaException;
010import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
011import gov.nist.secauto.metaschema.core.model.xml.ExternalConstraintsModulePostProcessor;
012import gov.nist.secauto.metaschema.core.util.CollectionUtil;
013import gov.nist.secauto.metaschema.core.util.ObjectUtils;
014import gov.nist.secauto.metaschema.databind.IBindingContext;
015import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader;
016import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader;
017
018import org.apache.maven.plugin.AbstractMojo;
019import org.apache.maven.plugin.MojoExecution;
020import org.apache.maven.plugin.MojoExecutionException;
021import org.apache.maven.plugins.annotations.Component;
022import org.apache.maven.plugins.annotations.Parameter;
023import org.apache.maven.project.MavenProject;
024import org.codehaus.plexus.util.DirectoryScanner;
025import org.sonatype.plexus.build.incremental.BuildContext;
026
027import java.io.File;
028import java.io.IOException;
029import java.net.URI;
030import java.util.ArrayList;
031import java.util.List;
032import java.util.Objects;
033import java.util.stream.Collectors;
034import java.util.stream.Stream;
035
036import edu.umd.cs.findbugs.annotations.NonNull;
037import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
038
039public abstract class AbstractMetaschemaMojo
040    extends AbstractMojo {
041  private static final String SYSTEM_FILE_ENCODING_PROPERTY = "file.encoding";
042  private static final String[] DEFAULT_INCLUDES = { "**/*.xml" };
043
044  /**
045   * The Maven project context.
046   *
047   * @parameter default-value="${project}"
048   * @required
049   * @readonly
050   */
051  @Parameter(defaultValue = "${project}", required = true, readonly = true)
052  MavenProject mavenProject;
053
054  /**
055   * This will be injected if this plugin is executed as part of the standard
056   * Maven lifecycle. If the mojo is directly invoked, this parameter will not be
057   * injected.
058   */
059  @Parameter(defaultValue = "${mojoExecution}", readonly = true)
060  private MojoExecution mojoExecution;
061
062  @Component
063  private BuildContext buildContext;
064
065  /**
066   * <p>
067   * The directory where the staleFile is found. The staleFile is used to
068   * determine if re-generation of generated Java classes is needed, by recording
069   * when the last build occurred.
070   * </p>
071   * <p>
072   * This directory is expected to be located within the
073   * <code>${project.build.directory}</code>, to ensure that code (re)generation
074   * occurs after cleaning the project.
075   * </p>
076   */
077  @Parameter(defaultValue = "${project.build.directory}/metaschema", readonly = true, required = true)
078  protected File staleFileDirectory;
079
080  /**
081   * <p>
082   * Defines the encoding used for generating Java Source files.
083   * </p>
084   * <p>
085   * The algorithm for finding the encoding to use is as follows (where the first
086   * non-null value found is used for encoding):
087   * <ol>
088   * <li>If the configuration property is explicitly given within the plugin's
089   * configuration, use that value.</li>
090   * <li>If the Maven property <code>project.build.sourceEncoding</code> is
091   * defined, use its value.</li>
092   * <li>Otherwise use the value from the system property
093   * <code>file.encoding</code>.</li>
094   * </ol>
095   * </p>
096   *
097   * @see #getEncoding()
098   * @since 2.0
099   */
100  @Parameter(defaultValue = "${project.build.sourceEncoding}")
101  private String encoding;
102
103  /**
104   * Location to generate Java source files in.
105   */
106  @Parameter(defaultValue = "${project.build.directory}/generated-sources/metaschema", required = true)
107  private File outputDirectory;
108
109  /**
110   * The directory to read source metaschema from.
111   */
112  @Parameter(defaultValue = "${basedir}/src/main/metaschema")
113  private File metaschemaDir;
114
115  /**
116   * A list of <code>files</code> containing Metaschema module constraints files.
117   */
118  @Parameter(property = "constraints")
119  private File[] constraints;
120
121  /**
122   * A set of inclusion patterns used to select which Metaschema modules are to be
123   * processed. By default, all files are processed.
124   */
125
126  @Parameter
127  protected String[] includes;
128
129  /**
130   * A set of exclusion patterns used to prevent certain files from being
131   * processed. By default, this set is empty such that no files are excluded.
132   */
133  @Parameter
134  protected String[] excludes;
135
136  /**
137   * Indicate if the execution should be skipped.
138   */
139  @Parameter(property = "metaschema.skip", defaultValue = "false")
140  private boolean skip;
141
142  /**
143   * The BuildContext is used to identify which files or directories were modified
144   * since last build. This is used to determine if Module-based generation must
145   * be performed again.
146   *
147   * @return the active Plexus BuildContext.
148   */
149  protected final BuildContext getBuildContext() {
150    return buildContext;
151  }
152
153  /**
154   * Retrieve the Maven project context.
155   *
156   * @return The active MavenProject.
157   */
158  protected final MavenProject getMavenProject() {
159    return mavenProject;
160  }
161
162  /**
163   * Retrieve the mojo execution context.
164   *
165   * @return The active MojoExecution.
166   */
167  @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "this is a data holder")
168  public MojoExecution getMojoExecution() {
169    return mojoExecution;
170  }
171
172  /**
173   * Retrieve the directory where generated classes will be stored.
174   *
175   * @return the directory
176   */
177  protected File getOutputDirectory() {
178    return outputDirectory;
179  }
180
181  /**
182   * Set the directory where generated classes will be stored.
183   *
184   * @param outputDirectory
185   *          the directory to use
186   */
187  protected void setOutputDirectory(File outputDirectory) {
188    Objects.requireNonNull(outputDirectory, "outputDirectory");
189    this.outputDirectory = outputDirectory;
190  }
191
192  /**
193   * Gets the file encoding to use for generated classes.
194   * <p>
195   * The algorithm for finding the encoding to use is as follows (where the first
196   * non-null value found is used for encoding):
197   * </p>
198   * <ol>
199   * <li>If the configuration property is explicitly given within the plugin's
200   * configuration, use that value.</li>
201   * <li>If the Maven property <code>project.build.sourceEncoding</code> is
202   * defined, use its value.</li>
203   * <li>Otherwise use the value from the system property
204   * <code>file.encoding</code>.</li>
205   * </ol>
206   *
207   * @return The encoding to be used by this AbstractJaxbMojo and its tools.
208   */
209  protected final String getEncoding() {
210    String encoding;
211    if (this.encoding != null) {
212      // first try to use the provided encoding
213      encoding = this.encoding;
214      if (getLog().isDebugEnabled()) {
215        getLog().debug(String.format("Using configured encoding [%s].", encoding));
216      }
217    } else {
218      encoding = System.getProperty(SYSTEM_FILE_ENCODING_PROPERTY);
219      if (getLog().isWarnEnabled()) {
220        getLog().warn(String.format("Using system encoding [%s]. This build is platform dependent!", encoding));
221      }
222    }
223    return encoding;
224  }
225
226  /**
227   * Retrieve a stream of Module file sources.
228   *
229   * @return the stream
230   */
231  protected Stream<File> getModuleSources() {
232    DirectoryScanner ds = new DirectoryScanner();
233    ds.setBasedir(metaschemaDir);
234    ds.setIncludes(includes != null && includes.length > 0 ? includes : DEFAULT_INCLUDES);
235    ds.setExcludes(excludes != null && excludes.length > 0 ? excludes : null);
236    ds.addDefaultExcludes();
237    ds.setCaseSensitive(true);
238    ds.setFollowSymlinks(false);
239    ds.scan();
240    return Stream.of(ds.getIncludedFiles()).map(filename -> new File(metaschemaDir, filename)).distinct();
241  }
242
243  /**
244   * Get the configured collection of constraints.
245   *
246   * @param bindingContext
247   *          the Metaschema binding context to use when loading the constraints
248   * @return the loaded constraints
249   * @throws MetaschemaException
250   *           if a binding exception occurred while loading the constraints
251   * @throws IOException
252   *           if an error occurred while reading the constraints
253   */
254  protected List<IConstraintSet> getConstraints(@NonNull IBindingContext bindingContext)
255      throws MetaschemaException, IOException {
256    IConstraintLoader loader = new BindingConstraintLoader(bindingContext);
257    List<IConstraintSet> constraintSets = new ArrayList<>(constraints.length);
258    for (File constraint : this.constraints) {
259      constraintSets.addAll(loader.load(ObjectUtils.notNull(constraint)));
260    }
261    return CollectionUtil.unmodifiableList(constraintSets);
262  }
263
264  /**
265   * Determine if the execution of this mojo should be skipped.
266   *
267   * @return {@code true} if the mojo execution should be skipped, or
268   *         {@code false} otherwise
269   */
270  protected boolean shouldExecutionBeSkipped() {
271    return skip;
272  }
273
274  /**
275   * Get the name of the file that is used to detect staleness.
276   *
277   * @return the name
278   */
279  protected abstract String getStaleFileName();
280
281  /**
282   * Gets the staleFile for this execution.
283   *
284   * @return the staleFile
285   */
286  protected final File getStaleFile() {
287    StringBuilder builder = new StringBuilder();
288    if (getMojoExecution() != null) {
289      builder.append(getMojoExecution().getExecutionId()).append('-');
290    }
291    builder.append(getStaleFileName());
292    return new File(staleFileDirectory, builder.toString());
293  }
294
295  /**
296   * Determine if code generation is required. This is done by comparing the last
297   * modified time of each Module source file against the stale file managed by
298   * this plugin.
299   *
300   * @return {@code true} if the code generation is needed, or {@code false}
301   *         otherwise
302   */
303  protected boolean isGenerationRequired() {
304    final File staleFile = getStaleFile();
305    boolean generate = !staleFile.exists();
306    if (generate) {
307      if (getLog().isInfoEnabled()) {
308        getLog().info(String.format("Stale file '%s' doesn't exist! Generating source files.", staleFile.getPath()));
309      }
310      generate = true;
311    } else {
312      generate = false;
313      // check for staleness
314      long staleLastModified = staleFile.lastModified();
315
316      BuildContext buildContext = getBuildContext();
317      URI metaschemaDirRelative = getMavenProject().getBasedir().toURI().relativize(metaschemaDir.toURI());
318
319      if (buildContext.isIncremental() && buildContext.hasDelta(metaschemaDirRelative.toString())) {
320        if (getLog().isInfoEnabled()) {
321          getLog().info("metaschemaDirRelative: " + metaschemaDirRelative.toString());
322        }
323        generate = true;
324      }
325
326      if (!generate) {
327        for (File sourceFile : getModuleSources().collect(Collectors.toList())) {
328          if (getLog().isInfoEnabled()) {
329            getLog().info("Source file: " + sourceFile.getPath());
330          }
331          if (sourceFile.lastModified() > staleLastModified) {
332            generate = true;
333          }
334        }
335      }
336    }
337    return generate;
338  }
339
340  /**
341   * Construct a new module loader based on the provided mojo configuration.
342   *
343   * @return the module loader
344   * @throws MojoExecutionException
345   *           if an error occurred while loading the configured constraints
346   */
347  @NonNull
348  protected BindingModuleLoader newModuleLoader() throws MojoExecutionException {
349    IBindingContext bindingContext = IBindingContext.instance();
350
351    List<IConstraintSet> constraints;
352    try {
353      constraints = getConstraints(bindingContext);
354    } catch (MetaschemaException | IOException ex) {
355      throw new MojoExecutionException("Unable to load external constraints.", ex);
356    }
357
358    // generate Java sources based on provided metaschema sources
359    BindingModuleLoader loader = constraints.isEmpty()
360        ? new BindingModuleLoader(bindingContext)
361        : new BindingModuleLoader(
362            bindingContext,
363            CollectionUtil.singletonList(new ExternalConstraintsModulePostProcessor(constraints)));
364    loader.allowEntityResolution();
365    return loader;
366  }
367}