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