1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.maven.plugin;
7   
8   import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
9   import gov.nist.secauto.metaschema.core.model.MetaschemaException;
10  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
11  import gov.nist.secauto.metaschema.core.model.xml.ExternalConstraintsModulePostProcessor;
12  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
13  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
14  import gov.nist.secauto.metaschema.databind.IBindingContext;
15  import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader;
16  import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader;
17  
18  import org.apache.maven.plugin.AbstractMojo;
19  import org.apache.maven.plugin.MojoExecution;
20  import org.apache.maven.plugin.MojoExecutionException;
21  import org.apache.maven.plugins.annotations.Component;
22  import org.apache.maven.plugins.annotations.Parameter;
23  import org.apache.maven.project.MavenProject;
24  import org.codehaus.plexus.util.DirectoryScanner;
25  import org.sonatype.plexus.build.incremental.BuildContext;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.net.URI;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.Objects;
33  import java.util.stream.Collectors;
34  import java.util.stream.Stream;
35  
36  import edu.umd.cs.findbugs.annotations.NonNull;
37  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
38  
39  public abstract class AbstractMetaschemaMojo
40      extends AbstractMojo {
41    private static final String SYSTEM_FILE_ENCODING_PROPERTY = "file.encoding";
42    private static final String[] DEFAULT_INCLUDES = { "**/*.xml" };
43  
44    /**
45     * The Maven project context.
46     *
47     * @parameter default-value="${project}"
48     * @required
49     * @readonly
50     */
51    @Parameter(defaultValue = "${project}", required = true, readonly = true)
52    MavenProject mavenProject;
53  
54    /**
55     * This will be injected if this plugin is executed as part of the standard
56     * Maven lifecycle. If the mojo is directly invoked, this parameter will not be
57     * injected.
58     */
59    @Parameter(defaultValue = "${mojoExecution}", readonly = true)
60    private MojoExecution mojoExecution;
61  
62    @Component
63    private BuildContext buildContext;
64  
65    /**
66     * <p>
67     * The directory where the staleFile is found. The staleFile is used to
68     * determine if re-generation of generated Java classes is needed, by recording
69     * when the last build occurred.
70     * </p>
71     * <p>
72     * This directory is expected to be located within the
73     * <code>${project.build.directory}</code>, to ensure that code (re)generation
74     * occurs after cleaning the project.
75     * </p>
76     */
77    @Parameter(defaultValue = "${project.build.directory}/metaschema", readonly = true, required = true)
78    protected File staleFileDirectory;
79  
80    /**
81     * <p>
82     * Defines the encoding used for generating Java Source files.
83     * </p>
84     * <p>
85     * The algorithm for finding the encoding to use is as follows (where the first
86     * non-null value found is used for encoding):
87     * <ol>
88     * <li>If the configuration property is explicitly given within the plugin's
89     * configuration, use that value.</li>
90     * <li>If the Maven property <code>project.build.sourceEncoding</code> is
91     * defined, use its value.</li>
92     * <li>Otherwise use the value from the system property
93     * <code>file.encoding</code>.</li>
94     * </ol>
95     * </p>
96     *
97     * @see #getEncoding()
98     * @since 2.0
99     */
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 }