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