View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.spi;
6   
7   import java.io.File;
8   import java.io.IOException;
9   import java.nio.charset.Charset;
10  import java.nio.charset.StandardCharsets;
11  import java.nio.file.Files;
12  import java.util.ArrayList;
13  import java.util.Arrays;
14  import java.util.Collection;
15  import java.util.Collections;
16  import java.util.HashMap;
17  import java.util.HashSet;
18  import java.util.Map;
19  import java.util.Set;
20  import org.apache.commons.io.FileUtils;
21  import org.apache.commons.io.filefilter.DirectoryFileFilter;
22  import org.apache.commons.io.filefilter.IOFileFilter;
23  import org.apache.commons.io.filefilter.WildcardFileFilter;
24  
25  /**
26   * Environment.
27   *
28   * @since 0.3
29   */
30  @SuppressWarnings("PMD.TooManyMethods")
31  public interface Environment {
32  
33      /**
34       * Get project's basedir.
35       *
36       * @return The directory
37       */
38      File basedir();
39  
40      /**
41       * Get directory to keep temporary files in.
42       *
43       * @return The directory
44       */
45      File tempdir();
46  
47      /**
48       * Get directory where {@code .class} files are stored.
49       *
50       * @return The directory
51       */
52      File outdir();
53  
54      /**
55       * Get the directories the project keeps its test sources in.
56       *
57       * <p>Maven allows a project to register any number of test source
58       * roots, either through {@code <testSourceDirectory>} or through a
59       * plugin such as {@code build-helper-maven-plugin:add-test-source},
60       * which the jcabi parent POM uses for {@code src/mock/java}. A file
61       * therefore cannot be told from a main one by its path alone, and
62       * whoever needs to know has to ask for these roots.</p>
63       *
64       * @return Test source roots, absolute, possibly empty
65       */
66      Collection<File> testdirs();
67  
68      /**
69       * Get parameter by name, and return default if it's not set.
70       *
71       * @param name The name of parameter
72       * @param value Default value to return as default
73       * @return The value
74       */
75      String param(String name, String value);
76  
77      /**
78       * Get classloader for this project.
79       *
80       * @return The classloader
81       */
82      ClassLoader classloader();
83  
84      /**
85       * Get list of paths in classpath.
86       *
87       * @return The collection of paths
88       */
89      Collection<String> classpath();
90  
91      /**
92       * Returns the files matching the specified pattern.
93       *
94       * <p>The pattern matching scheme used is wildcard matching. The characters
95       * '?' and '*' represents single or multiple wildcard characters,
96       * respectively. Pattern matching is case sensitive.</p>
97       *
98       * @param pattern File name pattern
99       * @return Collection of files, matching the specified pattern
100      */
101     Collection<File> files(String pattern);
102 
103     /**
104      * Shall this item be excluded from report?
105      *
106      * @param check Name of the check that is asking
107      * @param name File or any other item, which is subject of validation
108      * @return TRUE if it should be ignored
109      */
110     boolean exclude(String check, String name);
111 
112     /**
113      * List of exclude patterns for given checker.
114      * Each list element will contain exactly one exclude pattern which,
115      * depending on the plugin that uses the excludes might be either wildcard
116      * (CodeNarc) pattern or regex pattern (FindBugs).
117      *
118      * @param checker Name of the checker that is asking (pmd, codenarc ...)
119      * @return Exclude patterns
120      */
121     Collection<String> excludes(String checker);
122 
123     /**
124      * Encoding for the files.
125      *
126      * @return Source files charset
127      */
128     Charset encoding();
129 
130     /**
131      * Mock of {@link Environment}.
132      *
133      * @since 0.1
134      */
135     final class Mock implements Environment {
136 
137         /**
138          * The basedir.
139          */
140         private final File origin;
141 
142         /**
143          * Files for classpath.
144          */
145         private final Set<String> paths;
146 
147         /**
148          * Map of params.
149          */
150         private final Map<String, String> params;
151 
152         /**
153          * Test source roots, on top of the default {@code src/test}.
154          */
155         private final Collection<File> tests;
156 
157         /**
158          * Exclude patterns.
159          */
160         private String excl;
161 
162         /**
163          * Default constructor.
164          */
165         public Mock() {
166             this(Environment.Mock.temporary());
167         }
168 
169         /**
170          * Constructor with a ready basedir.
171          *
172          * @param base The basedir
173          */
174         private Mock(final File base) {
175             this(base, Environment.Mock.outputs(base));
176         }
177 
178         /**
179          * Primary constructor.
180          *
181          * @param base The basedir
182          * @param dirs Directories to put on the classpath
183          */
184         private Mock(final File base, final Set<String> dirs) {
185             this.origin = base;
186             this.paths = dirs;
187             this.params = new HashMap<>();
188             this.tests = new ArrayList<>(0);
189         }
190 
191         /**
192          * With this extra test source root, the way
193          * {@code build-helper-maven-plugin} would add one.
194          *
195          * @param path Directory name, related to basedir
196          * @return This object
197          */
198         public Environment.Mock withTestdir(final String path) {
199             this.tests.add(new File(this.origin, path));
200             return this;
201         }
202 
203         /**
204          * With this param and its value.
205          *
206          * @param name Param name
207          * @param value Param value
208          * @return This object
209          */
210         public Environment.Mock withParam(final String name,
211             final String value) {
212             this.params.put(name, value);
213             return this;
214         }
215 
216         /**
217          * With this file on board.
218          *
219          * @param name File name related to basedir
220          * @param content File content to write
221          * @return This object
222          * @throws IOException If some IO problem
223          */
224         public Environment.Mock withFile(final String name,
225             final String content) throws IOException {
226             FileUtils.writeStringToFile(
227                 new File(this.origin, name),
228                 content,
229                 StandardCharsets.UTF_8
230             );
231             return this;
232         }
233 
234         /**
235          * With this file on board.
236          *
237          * @param name File name related to basedir
238          * @param bytes File content to write
239          * @return This object
240          * @throws IOException If some IO problem
241          */
242         public Environment.Mock withFile(final String name,
243             final byte[] bytes) throws IOException {
244             FileUtils.writeByteArrayToFile(new File(this.origin, name), bytes);
245             return this;
246         }
247 
248         /**
249          * With exclude patterns.
250          *
251          * @param excludes Exclude patterns
252          * @return This object
253          */
254         public Environment.Mock withExcludes(final String excludes) {
255             this.excl = excludes;
256             return this;
257         }
258 
259         /**
260          * With default classpath.
261          *
262          * @return This object
263          */
264         public Environment.Mock withDefaultClasspath() {
265             Collections.addAll(
266                 this.paths,
267                 System.getProperty("java.class.path")
268                     .split(System.getProperty("path.separator"))
269             );
270             return this;
271         }
272 
273         @Override
274         public File basedir() {
275             return this.origin;
276         }
277 
278         @Override
279         public File tempdir() {
280             final File file = new File(this.origin, "target/tempdir");
281             if (!file.mkdirs() && !file.isDirectory()) {
282                 throw new IllegalStateException(
283                     String.format("cannot create tempdir at %s", file)
284                 );
285             }
286             return file;
287         }
288 
289         @Override
290         public File outdir() {
291             final File file = new File(this.origin, "target/classes");
292             if (!file.mkdirs() && !file.isDirectory()) {
293                 throw new IllegalStateException(
294                     String.format("cannot create outdir at %s", file)
295                 );
296             }
297             return file;
298         }
299 
300         @Override
301         public Collection<File> testdirs() {
302             final Collection<File> dirs = new ArrayList<>(this.tests);
303             dirs.add(new File(this.origin, "src/test"));
304             return dirs;
305         }
306 
307         @Override
308         public String param(final String name, final String value) {
309             String val = this.params.get(name);
310             if (val == null) {
311                 val = value;
312             }
313             return val;
314         }
315 
316         @Override
317         public ClassLoader classloader() {
318             return Thread.currentThread().getContextClassLoader();
319         }
320 
321         @Override
322         public Collection<String> classpath() {
323             return Collections.unmodifiableCollection(this.paths);
324         }
325 
326         @Override
327         public Collection<File> files(final String pattern) {
328             final Collection<File> files = new ArrayList<>(0);
329             final IOFileFilter filter = WildcardFileFilter.builder().setWildcards(pattern).get();
330             if (this.basedir().exists()) {
331                 for (final File found : FileUtils.listFiles(
332                     this.basedir(),
333                     filter,
334                     DirectoryFileFilter.INSTANCE
335                 )) {
336                     if (!new Binary(found).yes()) {
337                         files.add(found);
338                     }
339                 }
340             }
341             return files;
342         }
343 
344         @Override
345         public boolean exclude(final String check, final String name) {
346             return false;
347         }
348 
349         @Override
350         public Collection<String> excludes(final String checker) {
351             final Collection<String> exc;
352             if (this.excl == null) {
353                 exc = Collections.emptyList();
354             } else {
355                 exc = Arrays.asList(this.excl.split(","));
356             }
357             return exc;
358         }
359 
360         @Override
361         public Charset encoding() {
362             return StandardCharsets.UTF_8;
363         }
364 
365         private static File temporary() {
366             try {
367                 final File temp = Files.createTempDirectory("mock-qulice").toFile();
368                 FileUtils.forceDeleteOnExit(temp);
369                 final File base = new File(temp, "basedir");
370                 if (!base.mkdirs()) {
371                     throw new IllegalStateException(
372                         String.format("cannot create basedir at %s", base)
373                     );
374                 }
375                 return base;
376             } catch (final IOException ex) {
377                 throw new IllegalStateException("cannot create basedir", ex);
378             }
379         }
380 
381         private static Set<String> outputs(final File base) {
382             final File out = new File(base, "target/classes");
383             if (!out.mkdirs() && !out.isDirectory()) {
384                 throw new IllegalStateException(
385                     String.format("cannot create classes dir at %s", out)
386                 );
387             }
388             return new HashSet<>(
389                 Collections.singleton(
390                     out.getAbsolutePath().replace(File.separatorChar, '/')
391                 )
392             );
393         }
394     }
395 }