-
Notifications
You must be signed in to change notification settings - Fork 34
/
AbstractCheckMojo.java
491 lines (437 loc) · 18.7 KB
/
AbstractCheckMojo.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
/*
* (C) Copyright Uwe Schindler (Generics Policeman) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.thetaphi.forbiddenapis.maven;
import static de.thetaphi.forbiddenapis.Checker.Option.*;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.DirectoryScanner;
import de.thetaphi.forbiddenapis.Checker;
import de.thetaphi.forbiddenapis.Constants;
import de.thetaphi.forbiddenapis.ForbiddenApiException;
import de.thetaphi.forbiddenapis.Logger;
import de.thetaphi.forbiddenapis.ParseException;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.RetentionPolicy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLClassLoader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Base class for forbiddenapis Mojos.
* @since 1.0
*/
public abstract class AbstractCheckMojo extends AbstractMojo implements Constants {
/**
* Lists all files, which contain signatures and comments for forbidden API calls.
* The signatures are resolved against the compile classpath.
* @since 1.0
*/
@Parameter(required = false)
private File[] signaturesFiles;
/**
* Lists all Maven artifacts, which contain signatures and comments for forbidden API calls.
* The artifact needs to be specified like a Maven dependency. Resolution is not transitive.
* You can refer to plain text Maven artifacts ({@code type="txt"}, e.g., with a separate {@code classifier}):
* <pre>
* <signaturesArtifact>
* <groupId>org.apache.foobar</groupId>
* <artifactId>example</artifactId>
* <version>1.0</version>
* <classifier>signatures</classifier>
* <type>txt</type>
* </signaturesArtifact>
* </pre>
* Alternatively, refer to signatures files inside JAR artifacts. In that case, the additional
* parameter {@code path} has to be given:
* <pre>
* <signaturesArtifact>
* <groupId>org.apache.foobar</groupId>
* <artifactId>example</artifactId>
* <version>1.0</version>
* <type>jar</type>
* <path>path/inside/jar/file/signatures.txt</path>
* </signaturesArtifact>
* </pre>
* <p>The signatures are resolved against the compile classpath.
* @since 2.0
*/
@Parameter(required = false)
private SignaturesArtifact[] signaturesArtifacts;
/**
* Gives a multiline list of signatures, inline in the pom.xml. Use an XML CDATA section to do that!
* The signatures are resolved against the compile classpath.
* @since 1.0
*/
@Parameter(required = false)
private String signatures;
/**
* Specifies <a href="bundled-signatures.html">built-in signatures</a> files (e.g., deprecated APIs for specific Java versions,
* unsafe method calls using default locale, default charset,...)
* @since 1.0
*/
@Parameter(required = false)
private String[] bundledSignatures;
/**
* Fail the build, if the bundled ASM library cannot read the class file format
* of the runtime library or the runtime library cannot be discovered.
* @since 1.0
*/
@Parameter(required = false, defaultValue = "false")
private boolean failOnUnsupportedJava;
/**
* Fail the build, if a class referenced in the scanned code is missing. This requires
* that you pass the whole classpath including all dependencies to this Mojo
* (Maven does this by default).
* @since 1.0
*/
@Parameter(required = false, defaultValue = "true")
private boolean failOnMissingClasses;
/**
* Fail the build if a signature is not resolving. If this parameter is set to
* to false, then such signatures are ignored. Defaults to {@code true}.
* <p>When disabling this setting, the task still prints a warning to inform the user about
* broken signatures. This cannot be disabled. There is a second setting
* {@link #ignoreSignaturesOfMissingClasses} that can be used to silently ignore
* signatures that refer to methods or field in classes that are not on classpath,
* e.g. This is useful in multi-module Maven builds where a common set of signatures is used,
* that are not part of every sub-modules dependencies.
* @see #ignoreSignaturesOfMissingClasses)
* @deprecated The setting 'failOnUnresolvableSignatures' was deprecated and will be removed in next version. Use 'ignoreSignaturesOfMissingClasses' instead.
* @since 1.4
*/
@Deprecated
@Parameter(required = false, defaultValue = "true")
private boolean failOnUnresolvableSignatures;
/**
* If a class is missing while parsing signatures files, all methods and fields from this
* class are silently ignored. This is useful in multi-module Maven
* projects where only some modules have the dependency to which the signature file(s) apply.
* This settings prints no warning at all, so verify the signatures at least once with
* full dependencies.
* Defaults to {@code false}.
* @since 3.0
*/
@Parameter(required = false, defaultValue = "false")
private boolean ignoreSignaturesOfMissingClasses;
/**
* Fail the build if violations have been found. Defaults to {@code true}.
* @since 2.0
*/
@Parameter(required = false, property="forbiddenapis.failOnViolation", defaultValue = "true")
private boolean failOnViolation;
/**
* Disable the internal JVM classloading cache when getting bytecode from
* the classpath. This setting slows down checks, but <em>may</em> work around
* issues with other Mojos, that do not close their class loaders.
* If you get {@code FileNotFoundException}s related to non-existent JAR entries
* you can try to work around using this setting.
* @since 2.2
*/
@Parameter(required = false, defaultValue = "false")
private boolean disableClassloadingCache;
/**
* The default compiler target version used to expand references to bundled JDK signatures.
* E.g., if you use "jdk-deprecated", it will expand to this version.
* This setting should be identical to the target version used in the compiler plugin.
* @since 1.0
*/
@Parameter(required = false, defaultValue = "${maven.compiler.target}")
private String targetVersion;
/**
* The default compiler release version used to expand references to bundled JDK signatures.
* E.g., if you use "jdk-deprecated", it will expand to this version.
* This setting should be identical to the release version used in the compiler plugin starting with Java 9.
* If given, this setting is used in preference to {@link #targetVersion}.
* @since 3.1
*/
@Parameter(required = false, defaultValue = "${maven.compiler.release}")
private String releaseVersion;
/**
* List of patterns matching all class files to be parsed from the classesDirectory.
* Can be changed to e.g. exclude several files (using excludes).
* The default is a single include with pattern '**/*.class'
* @see #excludes
* @since 1.0
*/
@Parameter(required = false)
private String[] includes;
/**
* List of patterns matching class files to be excluded from checking.
* @see #includes
* @since 1.0
*/
@Parameter(required = false)
private String[] excludes;
/**
* List of a custom Java annotations (full class names) that are used in the checked
* code to suppress errors. Those annotations must have at least
* {@link RetentionPolicy#CLASS}. They can be applied to classes, their methods,
* or fields. By default, {@code @de.thetaphi.forbiddenapis.SuppressForbidden}
* can always be used, but needs the {@code forbidden-apis.jar} file in classpath
* of compiled project, which may not be wanted.
* Instead of a full class name, a glob pattern may be used (e.g.,
* {@code **.SuppressForbidden}).
* @since 1.8
*/
@Parameter(required = false)
private String[] suppressAnnotations;
/**
* Skip entire check. Most useful on the command line via "-Dforbiddenapis.skip=true".
* @since 1.6
*/
@Parameter(required = false, property="forbiddenapis.skip", defaultValue="false")
private boolean skip;
/** The project packaging (pom, jar, etc.). */
@Parameter(defaultValue = "${project.packaging}", readonly = true, required = true)
private String packaging;
@Component
private ArtifactFactory artifactFactory;
@Component
private ArtifactResolver artifactResolver;
@Parameter(defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true)
private List<ArtifactRepository> remoteRepositories;
@Parameter(defaultValue = "${localRepository}", readonly = true, required = true)
private ArtifactRepository localRepository;
/** provided by the concrete Mojos for compile and test classes processing */
protected abstract List<String> getClassPathElements();
/** provided by the concrete Mojos for compile and test classes processing */
protected abstract File getClassesDirectory();
/** gets overridden for test, because it uses testTargetVersion as optional name to override */
protected String getTargetVersion() {
return (releaseVersion != null) ? releaseVersion : targetVersion;
}
private File resolveSignaturesArtifact(SignaturesArtifact signaturesArtifact) throws ArtifactResolutionException, ArtifactNotFoundException {
final Artifact artifact = signaturesArtifact.createArtifact(artifactFactory);
artifactResolver.resolve(artifact, this.remoteRepositories, this.localRepository);
final File f = artifact.getFile();
// Can this ever be false? Be sure. Found the null check also in other Maven code, so be safe!
if (f == null) {
throw new ArtifactNotFoundException("Artifact does not resolve to a file.", artifact);
}
return f;
}
private String encodeUrlPath(String path) {
try {
// hack to encode the URL path by misusing URI class:
return new URI(null, path, null).toASCIIString();
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
private URL createJarUrl(File f, String jarPath) throws MalformedURLException {
final URL fileUrl = f.toURI().toURL();
final URL jarBaseUrl = new URL("jar", null, fileUrl.toExternalForm() + "!/");
return new URL(jarBaseUrl, encodeUrlPath(jarPath));
}
@Override
public void execute() throws MojoExecutionException {
final Logger log = new Logger() {
@Override
public void error(String msg) {
getLog().error(msg);
}
@Override
public void warn(String msg) {
getLog().warn(msg);
}
@Override
public void info(String msg) {
getLog().info(msg);
}
@Override
public void debug(String msg) {
getLog().debug(msg);
}
};
if (skip) {
log.info("Skipping forbidden-apis checks.");
return;
}
// In multi-module projects, one may want to configure the plugin in the parent/root POM.
// However, it should not be executed for this type of POMs.
if ("pom".equals(packaging)) {
log.info("Skipping execution for packaging \"" + packaging + "\"");
return;
}
// set default param:
if (includes == null) includes = new String[] {"**/*.class"};
final List<String> cp = getClassPathElements();
final URL[] urls = new URL[cp.size()];
final StringBuilder humanClasspath = new StringBuilder();
try {
int i = 0;
for (final String cpElement : cp) {
urls[i++] = new File(cpElement).toURI().toURL();
if (humanClasspath.length() > 0) {
humanClasspath.append(File.pathSeparatorChar);
}
humanClasspath.append(cpElement);
}
assert i == urls.length;
} catch (MalformedURLException e) {
throw new MojoExecutionException("Failed to build classpath.", e);
}
log.debug("Classpath: " + humanClasspath);
URLClassLoader urlLoader = null;
final ClassLoader loader = (urls.length > 0) ?
(urlLoader = URLClassLoader.newInstance(urls, ClassLoader.getSystemClassLoader())) :
ClassLoader.getSystemClassLoader();
try {
final EnumSet<Checker.Option> options = EnumSet.noneOf(Checker.Option.class);
if (failOnMissingClasses) options.add(FAIL_ON_MISSING_CLASSES);
if (failOnViolation) options.add(FAIL_ON_VIOLATION);
if (failOnUnresolvableSignatures) {
options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES);
} else {
log.warn(DEPRECATED_WARN_FAIL_ON_UNRESOLVABLE_SIGNATURES);
}
if (ignoreSignaturesOfMissingClasses) options.add(IGNORE_SIGNATURES_OF_MISSING_CLASSES);
if (disableClassloadingCache) options.add(DISABLE_CLASSLOADING_CACHE);
final Checker checker = new Checker(log, loader, options);
if (!checker.isSupportedJDK) {
final String msg = String.format(Locale.ENGLISH,
"Your Java runtime (%s %s) is not supported by the forbiddenapis MOJO. Please run the checks with a supported JDK!",
System.getProperty("java.runtime.name"), System.getProperty("java.runtime.version"));
if (failOnUnsupportedJava) {
throw new MojoExecutionException(msg);
} else {
log.warn(msg);
return;
}
}
if (suppressAnnotations != null) {
for (String a : suppressAnnotations) {
checker.addSuppressAnnotation(a);
}
}
log.info("Scanning for classes to check...");
final File classesDirectory = getClassesDirectory();
if (!classesDirectory.exists()) {
log.info("Classes directory does not exist, forbiddenapis check skipped: " + classesDirectory);
return;
}
final DirectoryScanner ds = new DirectoryScanner();
ds.setBasedir(classesDirectory);
ds.setCaseSensitive(true);
ds.setIncludes(includes);
ds.setExcludes(excludes);
ds.addDefaultExcludes();
ds.scan();
final String[] files = ds.getIncludedFiles();
if (files.length == 0) {
log.info(String.format(Locale.ENGLISH,
"No classes found in '%s' (includes=%s, excludes=%s), forbiddenapis check skipped.",
classesDirectory.toString(), Arrays.toString(includes), Arrays.toString(excludes)));
return;
}
try {
if (bundledSignatures != null) {
String targetVersion = getTargetVersion();
if ("".equals(targetVersion)) targetVersion = null;
if (targetVersion == null) {
log.warn("The 'targetVersion' and 'targetRelease' parameters or " +
"'${maven.compiler.target}' and '${maven.compiler.release}' properties are missing. " +
"Trying to read bundled JDK signatures without compiler target. " +
"You have to explicitly specify the version in the resource name.");
}
for (String bs : new LinkedHashSet<>(Arrays.asList(bundledSignatures))) {
checker.addBundledSignatures(bs, targetVersion);
}
}
final Set<File> sigFiles = new LinkedHashSet<>();
final Set<URL> sigUrls = new LinkedHashSet<>();
if (signaturesFiles != null) {
sigFiles.addAll(Arrays.asList(signaturesFiles));
}
if (signaturesArtifacts != null) {
for (final SignaturesArtifact artifact : signaturesArtifacts) {
final File f = resolveSignaturesArtifact(artifact);
if (artifact.path != null) {
if (f.isDirectory()) {
// if Maven did not yet jarred the artifact, it returns the classes
// folder of the foreign Maven project, just use that one:
sigFiles.add(new File(f, artifact.path));
} else {
sigUrls.add(createJarUrl(f, artifact.path));
}
} else {
sigFiles.add(f);
}
}
}
for (final File f : sigFiles) {
checker.parseSignaturesFile(f);
}
for (final URL u : sigUrls) {
checker.parseSignaturesFile(u);
}
final String sig = (signatures != null) ? signatures.trim() : null;
if (sig != null && sig.length() != 0) {
checker.parseSignaturesString(sig);
}
} catch (IOException ioe) {
throw new MojoExecutionException("IO problem while reading files with API signatures.", ioe);
} catch (ParseException pe) {
throw new MojoExecutionException("Parsing signatures failed: " + pe.getMessage(), pe);
} catch (ArtifactResolutionException e) {
throw new MojoExecutionException("Problem while resolving Maven artifact.", e);
} catch (ArtifactNotFoundException e) {
throw new MojoExecutionException("Maven artifact does not exist.", e);
}
if (checker.hasNoSignatures()) {
if (checker.noSignaturesFilesParsed()) {
throw new MojoExecutionException("No signatures were added to mojo; use parameters 'signatures', 'bundledSignatures', 'signaturesFiles', and/or 'signaturesArtifacts' to define those!");
} else {
log.info("Skipping execution because no API signatures are available.");
return;
}
}
try {
checker.addClassesToCheck(classesDirectory, files);
} catch (IOException ioe) {
throw new MojoExecutionException("Failed to load one of the given class files.", ioe);
}
try {
checker.run();
} catch (ForbiddenApiException fae) {
throw new MojoExecutionException(fae.getMessage(), fae.getCause());
}
} finally {
// Close the classloader to free resources:
try {
if (urlLoader != null) urlLoader.close();
} catch (IOException ioe) {
log.warn("Cannot close classloader: ".concat(ioe.toString()));
}
}
}
}