View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.maven;
6   
7   import com.google.common.base.Function;
8   import com.google.common.base.Predicate;
9   import com.google.common.base.Predicates;
10  import com.google.common.collect.Collections2;
11  import com.google.common.collect.Iterables;
12  import com.jcabi.log.Logger;
13  import com.qulice.spi.Binary;
14  import java.io.File;
15  import java.net.MalformedURLException;
16  import java.net.URI;
17  import java.net.URL;
18  import java.net.URLClassLoader;
19  import java.nio.charset.Charset;
20  import java.security.PrivilegedAction;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.List;
24  import java.util.Properties;
25  import javax.annotation.Nullable;
26  import org.apache.commons.io.FileUtils;
27  import org.apache.commons.io.FilenameUtils;
28  import org.apache.commons.io.filefilter.DirectoryFileFilter;
29  import org.apache.commons.io.filefilter.IOFileFilter;
30  import org.apache.commons.io.filefilter.WildcardFileFilter;
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.artifact.DependencyResolutionRequiredException;
33  import org.apache.maven.model.Build;
34  import org.apache.maven.model.Resource;
35  import org.apache.maven.project.MavenProject;
36  import org.codehaus.plexus.context.Context;
37  
38  /**
39   * Environment, passed from MOJO to validators.
40   * @since 0.3
41   */
42  @SuppressWarnings("PMD.GodClass")
43  public final class DefaultMavenEnvironment implements MavenEnvironment {
44  
45      /**
46       * Maven project.
47       */
48      private MavenProject iproject;
49  
50      /**
51       * Plexus context.
52       */
53      private Context icontext;
54  
55      /**
56       * Plugin configuration.
57       */
58      private final Properties iproperties;
59  
60      /**
61       * MOJO executor.
62       */
63      private MojoExecutor exectr;
64  
65      /**
66       * Excludes, regular expressions.
67       */
68      private final Collection<String> exc;
69  
70      /**
71       * Xpath queries for pom.xml validation.
72       */
73      private final Collection<String> assertion;
74  
75      /**
76       * Source code encoding charset.
77       */
78      private String charset;
79  
80      /**
81       * Default constructor.
82       */
83      public DefaultMavenEnvironment() {
84          this.iproperties = new Properties();
85          this.exc = new ArrayList<>(0);
86          this.assertion = new ArrayList<>(0);
87          this.charset = "UTF-8";
88      }
89  
90      @Override
91      public String param(final String name, final String value) {
92          String ret = this.iproperties.getProperty(name);
93          if (ret == null) {
94              ret = value;
95          }
96          return ret;
97      }
98  
99      @Override
100     public File basedir() {
101         return this.iproject.getBasedir();
102     }
103 
104     @Override
105     public File tempdir() {
106         return new File(this.iproject.getBuild().getOutputDirectory());
107     }
108 
109     @Override
110     public File outdir() {
111         return new File(this.iproject.getBuild().getOutputDirectory());
112     }
113 
114     @Override
115     @SuppressWarnings("deprecation")
116     public Collection<String> classpath() {
117         final Collection<String> paths = new ArrayList<>(0);
118         final String blank = "%20";
119         final String whitespace = " ";
120         try {
121             for (final String name
122                 : this.iproject.getRuntimeClasspathElements()) {
123                 paths.add(
124                     name.replace(
125                         File.separatorChar, '/'
126                     ).replaceAll(whitespace, blank)
127                 );
128             }
129             for (final Artifact artifact
130                 : this.iproject.getDependencyArtifacts()) {
131                 if (artifact.getFile() != null) {
132                     paths.add(
133                         artifact.getFile().getAbsolutePath()
134                             .replace(File.separatorChar, '/')
135                             .replaceAll(whitespace, blank)
136                     );
137                 }
138             }
139         } catch (final DependencyResolutionRequiredException ex) {
140             throw new IllegalStateException("Failed to read classpath", ex);
141         }
142         return paths;
143     }
144 
145     @Override
146     public ClassLoader classloader() {
147         final List<URL> urls = new ArrayList<>(0);
148         for (final String path : this.classpath()) {
149             try {
150                 urls.add(
151                     URI.create(String.format("file:///%s", path)).toURL()
152                 );
153             } catch (final MalformedURLException ex) {
154                 throw new IllegalStateException("Failed to build URL", ex);
155             }
156         }
157         final URLClassLoader loader =
158             new DefaultMavenEnvironment.PrivilegedClassLoader(urls).run();
159         for (final URL url : loader.getURLs()) {
160             Logger.debug(this, "Classpath: %s", url);
161         }
162         return loader;
163     }
164 
165     @Override
166     public MavenProject project() {
167         return this.iproject;
168     }
169 
170     @Override
171     public Properties properties() {
172         return this.iproperties;
173     }
174 
175     @Override
176     public Context context() {
177         return this.icontext;
178     }
179 
180     @Override
181     public Properties config() {
182         return this.iproperties;
183     }
184 
185     @Override
186     public MojoExecutor executor() {
187         return this.exectr;
188     }
189 
190     @Override
191     public Collection<String> asserts() {
192         return this.assertion;
193     }
194 
195     @Override
196     public Collection<File> files(final String pattern) {
197         final Collection<File> files = new ArrayList<>(0);
198         final IOFileFilter filter = WildcardFileFilter.builder().setWildcards(pattern).get();
199         for (final File sources : this.sources()) {
200             if (sources.exists()) {
201                 for (final File found : FileUtils.listFiles(
202                     sources,
203                     filter,
204                     DirectoryFileFilter.INSTANCE
205                 )) {
206                     if (new Binary(found).yes()) {
207                         Logger.debug(
208                             this,
209                             "Skipping binary file %s",
210                             found
211                         );
212                     } else {
213                         files.add(found);
214                     }
215                 }
216             }
217         }
218         return files;
219     }
220 
221     @Override
222     public boolean exclude(final String check, final String name) {
223         return Iterables.any(
224             this.excludes(check),
225             new DefaultMavenEnvironment.PathPredicate(name)
226         );
227     }
228 
229     @Override
230     public Collection<String> excludes(final String checker) {
231         return Collections2.filter(
232             Collections2.transform(
233                 this.exc,
234                 new DefaultMavenEnvironment.CheckerExcludes(checker)
235             ),
236             Predicates.notNull()
237         );
238     }
239 
240     /**
241      * Set Maven Project (used mostly for unit testing).
242      * @param proj The project to set
243      */
244     public void setProject(final MavenProject proj) {
245         this.iproject = proj;
246     }
247 
248     /**
249      * Set context.
250      * @param ctx The context to set
251      */
252     public void setContext(final Context ctx) {
253         this.icontext = ctx;
254     }
255 
256     /**
257      * Set executor.
258      * @param exec The executor
259      */
260     public void setMojoExecutor(final MojoExecutor exec) {
261         this.exectr = exec;
262     }
263 
264     /**
265      * Set property.
266      * @param name Its name
267      * @param value Its value
268      */
269     public void setProperty(final String name, final String value) {
270         this.iproperties.setProperty(name, value);
271     }
272 
273     /**
274      * Set list of regular expressions to exclude.
275      * @param exprs Expressions
276      */
277     public void setExcludes(final Collection<String> exprs) {
278         this.exc.clear();
279         this.exc.addAll(exprs);
280     }
281 
282     /**
283      * Set list of Xpath queries for pom.xml validation.
284      * @param ass Xpath queries
285      */
286     public void setAssertion(final Collection<String> ass) {
287         this.assertion.clear();
288         this.assertion.addAll(ass);
289     }
290 
291     /**
292      * Set the encoding of source files.
293      * @param encoding The encoding to use
294      */
295     public void setEncoding(final String encoding) {
296         this.charset = encoding;
297     }
298 
299     @Override
300     public Charset encoding() {
301         if (this.charset == null || this.charset.isEmpty()) {
302             this.charset = "UTF-8";
303         }
304         return Charset.forName(this.charset);
305     }
306 
307     /**
308      * Collect source directories declared by the Maven project.
309      *
310      * <p>Uses compile and test source roots together with declared resources
311      * so that a project configuring {@code <sourceDirectory>} or
312      * {@code <testSourceDirectory>} in its POM is honored. Falls back to
313      * {@code src} under the basedir when the project exposes no directories,
314      * which preserves the historical behavior for minimal stubs. Roots that
315      * live under the project's build directory (e.g.
316      * {@code target/generated-sources/...}) are filtered out, since those
317      * are generated build outputs and not user-authored code (issue #1560).
318      * </p>
319      *
320      * @return Absolute directories to scan for files
321      */
322     private Collection<File> sources() {
323         final Collection<File> dirs = new ArrayList<>(0);
324         final Build build = this.iproject.getBuild();
325         final File output = this.buildDirectory(build);
326         this.addRoots(dirs, this.iproject.getCompileSourceRoots(), output);
327         this.addRoots(dirs, this.iproject.getTestCompileSourceRoots(), output);
328         if (build != null) {
329             this.addResources(dirs, build.getResources(), output);
330             this.addResources(dirs, build.getTestResources(), output);
331         }
332         if (dirs.isEmpty()) {
333             dirs.add(new File(this.basedir(), "src"));
334         }
335         return dirs;
336     }
337 
338     /**
339      * Resolve the project's build directory.
340      * @param build Build descriptor, may be null
341      * @return Canonical build directory, or null if not declared
342      */
343     @Nullable
344     private File buildDirectory(@Nullable final Build build) {
345         File dir = null;
346         if (build != null && build.getDirectory() != null) {
347             dir = this.resolve(build.getDirectory());
348         }
349         return dir;
350     }
351 
352     /**
353      * Add resolved roots to the given collection.
354      * @param dirs Collection to fill
355      * @param roots Source roots, may be null
356      * @param output Build output directory, may be null
357      */
358     private void addRoots(final Collection<File> dirs,
359         final List<String> roots, @Nullable final File output) {
360         if (roots != null) {
361             for (final String root : roots) {
362                 final File resolved = this.resolve(root);
363                 if (DefaultMavenEnvironment.outside(resolved, output)) {
364                     dirs.add(resolved);
365                 } else {
366                     Logger.debug(
367                         this,
368                         "Skipping generated source root %s under %s",
369                         resolved, output
370                     );
371                 }
372             }
373         }
374     }
375 
376     /**
377      * Add resolved resource directories to the given collection.
378      * @param dirs Collection to fill
379      * @param resources Resources, may be null
380      * @param output Build output directory, may be null
381      */
382     private void addResources(final Collection<File> dirs,
383         final List<Resource> resources, @Nullable final File output) {
384         if (resources != null) {
385             for (final Resource res : resources) {
386                 final File resolved = this.resolve(res.getDirectory());
387                 if (DefaultMavenEnvironment.outside(resolved, output)) {
388                     dirs.add(resolved);
389                 } else {
390                     Logger.debug(
391                         this,
392                         "Skipping generated resource directory %s under %s",
393                         resolved, output
394                     );
395                 }
396             }
397         }
398     }
399 
400     /**
401      * Check that the file does not live inside the given parent.
402      * @param file Candidate
403      * @param parent Possibly enclosing directory, may be null
404      * @return True when the file is outside the parent
405      */
406     private static boolean outside(final File file,
407         @Nullable final File parent) {
408         boolean answer = true;
409         if (parent != null) {
410             final String head = FilenameUtils.normalize(
411                 parent.getAbsolutePath(), true
412             );
413             final String tail = FilenameUtils.normalize(
414                 file.getAbsolutePath(), true
415             );
416             if (head != null && tail != null
417                 && (tail.equals(head) || tail.startsWith(head.concat("/")))) {
418                 answer = false;
419             }
420         }
421         return answer;
422     }
423 
424     /**
425      * Resolve a directory path against the project basedir.
426      * @param path Absolute or relative path
427      * @return Absolute file
428      */
429     private File resolve(final String path) {
430         final File file = new File(path);
431         final File resolved;
432         if (file.isAbsolute()) {
433             resolved = file;
434         } else {
435             resolved = new File(this.basedir(), path);
436         }
437         return resolved;
438     }
439 
440     /**
441      * Creates URL ClassLoader in privileged block.
442      * @since 0.1
443      */
444     private static final class PrivilegedClassLoader implements
445         PrivilegedAction<URLClassLoader> {
446 
447         /**
448          * URLs for class loading.
449          */
450         private final List<URL> urls;
451 
452         /**
453          * Constructor.
454          * @param urls URLs for class loading
455          */
456         private PrivilegedClassLoader(final List<URL> urls) {
457             this.urls = urls;
458         }
459 
460         @Override
461         public URLClassLoader run() {
462             return new URLClassLoader(
463                 this.urls.toArray(new URL[0]),
464                 Thread.currentThread().getContextClassLoader()
465             );
466         }
467     }
468 
469     /**
470      * Checks if two paths are equal.
471      * @since 0.1
472      */
473     private static class PathPredicate implements Predicate<String> {
474 
475         /**
476          * Path to match.
477          */
478         private final String name;
479 
480         /**
481          * Constructor.
482          * @param name Path to match
483          */
484         PathPredicate(final String name) {
485             this.name = name;
486         }
487 
488         @Override
489         public boolean apply(@Nullable final String input) {
490             return input != null
491                 && FilenameUtils.normalize(this.name, true).matches(input);
492         }
493     }
494 
495     /**
496      * Converts a checker exclude into exclude param.
497      *
498      * E.g. "checkstyle:.*" will become ".*".
499      *
500      * @since 0.1
501      */
502     private static class CheckerExcludes implements Function<String, String> {
503 
504         /**
505          * All checkers.
506          */
507         private static final String ALL = "*";
508 
509         /**
510          * Name of checker.
511          */
512         private final String checker;
513 
514         /**
515          * Constructor.
516          * @param checker Name of checker
517          */
518         CheckerExcludes(final String checker) {
519             this.checker = checker;
520         }
521 
522         @Nullable
523         @Override
524         public String apply(@Nullable final String input) {
525             String result = null;
526             if (input != null) {
527                 final String[] exclude = input.split(":", 2);
528                 final String check = exclude[0];
529                 final boolean appropriate = CheckerExcludes.ALL.equals(check)
530                     || this.checker.equals(check);
531                 if (appropriate && exclude.length > 1) {
532                     result = exclude[1];
533                 }
534             }
535             return result;
536         }
537     }
538 }