View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.jdeps;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.nio.file.Path;
26  import java.nio.file.Paths;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.LinkedHashSet;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Properties;
34  import java.util.Set;
35  import java.util.StringTokenizer;
36  
37  import org.apache.commons.lang3.StringUtils;
38  import org.apache.commons.lang3.SystemUtils;
39  import org.apache.maven.artifact.Artifact;
40  import org.apache.maven.artifact.ArtifactUtils;
41  import org.apache.maven.artifact.DependencyResolutionRequiredException;
42  import org.apache.maven.execution.MavenSession;
43  import org.apache.maven.plugin.AbstractMojo;
44  import org.apache.maven.plugin.MojoExecutionException;
45  import org.apache.maven.plugin.MojoFailureException;
46  import org.apache.maven.plugins.annotations.Parameter;
47  import org.apache.maven.plugins.jdeps.consumers.JDepsConsumer;
48  import org.apache.maven.project.MavenProject;
49  import org.apache.maven.toolchain.Toolchain;
50  import org.apache.maven.toolchain.ToolchainManager;
51  import org.codehaus.plexus.util.MatchPatterns;
52  import org.codehaus.plexus.util.cli.CommandLineException;
53  import org.codehaus.plexus.util.cli.CommandLineUtils;
54  import org.codehaus.plexus.util.cli.Commandline;
55  
56  /**
57   * Abstract Mojo for JDeps
58   *
59   * @author Robert Scholte
60   *
61   */
62  public abstract class AbstractJDepsMojo extends AbstractMojo {
63  
64      @Parameter(defaultValue = "${project}", readonly = true, required = true)
65      private MavenProject project;
66  
67      @Parameter(defaultValue = "${session}", readonly = true, required = true)
68      private MavenSession session;
69  
70      @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true)
71      private File outputDirectory;
72  
73      /**
74       * Indicates whether the build will continue even if there are jdeps warnings.
75       */
76      @Parameter(defaultValue = "true", property = "jdeps.failOnWarning")
77      private boolean failOnWarning;
78  
79      /**
80       * Specifies the version when processing multi-release JAR files version should be an integer >=9 or base.
81       *
82       * @since 3.1.1
83       */
84      @Parameter(property = "jdeps.multiRelease")
85      private String multiRelease;
86  
87      /**
88       * Whether only the sources need to be compatible or also every dependency on the classpath.
89       *
90       * @since 3.1.3
91       */
92      @Parameter(defaultValue = "true", property = "jdeps.includeClasspath")
93      private boolean includeClasspath;
94  
95      /**
96       * Additional dependencies which should be analyzed besides the classes.
97       * Specify as {@code groupId:artifactId}, allowing ant-pattern.
98       *
99       * E.g.
100      * <pre>
101      *   &lt;dependenciesToAnalyzeIncludes&gt;
102      *     &lt;include&gt;*:*&lt;/include&gt;
103      *     &lt;include&gt;org.foo.*:*&lt;/include&gt;
104      *     &lt;include&gt;com.foo.bar:*&lt;/include&gt;
105      *     &lt;include&gt;dot.foo.bar:utilities&lt;/include&gt;
106      *   &lt;/dependenciesToAnalyzeIncludes&gt;
107      * </pre>
108      */
109     @Parameter
110     private List<String> dependenciesToAnalyzeIncludes;
111 
112     /**
113      * Subset of {@link AbstractJDepsMojo#dependenciesToAnalyzeIncludes} which should be not analyzed.
114      * Specify as {@code groupId:artifactId}, allowing ant-pattern.
115      *
116      * E.g.
117      * <pre>
118      *   &lt;dependenciesToAnalyzeExcludes&gt;
119      *     &lt;exclude&gt;org.foo.*:*&lt;/exclude&gt;
120      *     &lt;exclude&gt;com.foo.bar:*&lt;/exclude&gt;
121      *     &lt;exclude&gt;dot.foo.bar:utilities&lt;/exclude&gt;
122      *   &lt;/dependenciesToAnalyzeExcludes&gt;
123      * </pre>
124      */
125     @Parameter
126     private List<String> dependenciesToAnalyzeExcludes;
127 
128     /**
129      * Destination directory for DOT file output
130      */
131     @Parameter(property = "jdeps.dotOutput")
132     private File dotOutput;
133 
134     /**
135      * <dl>
136      *   <dt>package</dt><dd>Print package-level dependencies excluding dependencies within the same archive<dd/>
137      *   <dt>class</dt><dd>Print class-level dependencies excluding dependencies within the same archive<dd/>
138      *   <dt>&lt;empty&gt;</dt><dd>Print all class level dependencies. Equivalent to -verbose:class -filter:none.<dd/>
139      * </dl>
140      */
141     @Parameter(property = "jdeps.verbose")
142     private String verbose;
143 
144     /**
145      * Finds dependences matching the specified package name.
146      *
147      * @since 3.1.1.
148      */
149     @Parameter
150     private List<String> packages;
151 
152     /**
153      * Restrict analysis to classes matching pattern. This option filters the list of classes to be analyzed. It can be
154      * used together with <code>-p</code> and <code>-e</code> which apply pattern to the dependences
155      */
156     @Parameter(property = "jdeps.include")
157     private String include;
158 
159     /**
160      * Restrict analysis to APIs i.e. dependences from the signature of public and protected members of public classes
161      * including field type, method parameter types, returned type, checked exception types etc
162      */
163     @Parameter(defaultValue = "false", property = "jdeps.apionly")
164     private boolean apiOnly;
165 
166     /**
167      * Show profile or the file containing a package
168      */
169     @Parameter(defaultValue = "false", property = "jdeps.profile")
170     private boolean profile;
171 
172     /**
173      * Recursively traverse all dependencies. The {@code -R} option implies {@code -filter:none}.  If {@code -p},
174      * {@code -e}, {@code -f} option is specified, only the matching dependences are analyzed.
175      */
176     @Parameter(defaultValue = "false", property = "jdeps.recursive")
177     private boolean recursive;
178 
179     /**
180      * Specifies the root module for analysis.
181      *
182      * @since JDK 1.9.0
183      */
184     @Parameter(property = "jdeps.module")
185     private String module;
186 
187     /**
188      * Show only internal API usage.
189      *
190      * @since 3.2.0
191      */
192     @Parameter(defaultValue = "false", property = "jdeps.jdkinternals")
193     private boolean jdkinternals;
194 
195     private final ToolchainManager toolchainManager;
196 
197     protected AbstractJDepsMojo(ToolchainManager toolchainManager) {
198         this.toolchainManager = toolchainManager;
199     }
200 
201     protected MavenProject getProject() {
202         return project;
203     }
204 
205     public void execute() throws MojoExecutionException, MojoFailureException {
206         if (!new File(getClassesDirectory()).exists()) {
207             getLog().debug("No classes to analyze");
208             return;
209         }
210 
211         String jExecutable;
212         try {
213             jExecutable = getJDepsExecutable();
214         } catch (IOException e) {
215             throw new MojoFailureException("Unable to find jdeps command: " + e.getMessage(), e);
216         }
217 
218         //      Synopsis
219         //      jdeps [options] classes ...
220         Commandline cmd = new Commandline();
221         cmd.setExecutable(jExecutable);
222 
223         Set<Path> dependenciesToAnalyze = null;
224         try {
225             dependenciesToAnalyze = getDependenciesToAnalyze(includeClasspath);
226         } catch (DependencyResolutionRequiredException e) {
227             throw new MojoExecutionException(e.getMessage(), e);
228         }
229         addJDepsOptions(cmd, dependenciesToAnalyze);
230         addJDepsClasses(cmd, dependenciesToAnalyze);
231 
232         JDepsConsumer consumer = new JDepsConsumer();
233         executeJDepsCommandLine(cmd, outputDirectory, consumer);
234 
235         // @ TODO if there will be more goals, this should be pushed down to AbstractJDKInternals
236         if (!consumer.getOffendingPackages().isEmpty()) {
237             final String ls = System.lineSeparator();
238 
239             StringBuilder msg = new StringBuilder();
240             msg.append("Found offending packages:").append(ls);
241             for (Map.Entry<String, String> offendingPackage :
242                     consumer.getOffendingPackages().entrySet()) {
243                 msg.append(' ')
244                         .append(offendingPackage.getKey())
245                         .append(" -> ")
246                         .append(offendingPackage.getValue())
247                         .append(ls);
248             }
249 
250             if (isFailOnWarning()) {
251                 throw new MojoExecutionException(msg.toString());
252             }
253         }
254     }
255 
256     protected void addJDepsOptions(Commandline cmd, Set<Path> dependenciesToAnalyze) throws MojoFailureException {
257         if (dotOutput != null) {
258             cmd.createArg().setValue("-dotoutput");
259             cmd.createArg().setFile(dotOutput);
260         }
261 
262         if (verbose != null) {
263             if ("class".equals(verbose)) {
264                 cmd.createArg().setValue("-verbose:class");
265             } else if ("package".equals(verbose)) {
266                 cmd.createArg().setValue("-verbose:package");
267             } else {
268                 cmd.createArg().setValue("-v");
269             }
270         }
271 
272         try {
273             Collection<Path> cp = new ArrayList<>();
274 
275             for (Path path : getClassPath()) {
276                 if (!dependenciesToAnalyze.contains(path)) {
277                     cp.add(path);
278                 }
279             }
280 
281             if (!cp.isEmpty()) {
282                 cmd.createArg().setValue("-cp");
283 
284                 cmd.createArg().setValue(StringUtils.join(cp.iterator(), File.pathSeparator));
285             }
286 
287         } catch (DependencyResolutionRequiredException e) {
288             throw new MojoFailureException(e.getMessage(), e);
289         }
290 
291         if (packages != null) {
292             for (String pkgName : packages) {
293                 cmd.createArg().setValue("-p");
294                 cmd.createArg().setValue(pkgName);
295             }
296         }
297 
298         if (include != null) {
299             cmd.createArg().setValue("-include");
300             cmd.createArg().setValue(include);
301         }
302 
303         if (profile) {
304             cmd.createArg().setValue("-P");
305         }
306 
307         if (module != null) {
308             cmd.createArg().setValue("-m");
309             cmd.createArg().setValue(module);
310         }
311 
312         if (multiRelease != null) {
313             cmd.createArg().setValue("--multi-release");
314             cmd.createArg().setValue(multiRelease);
315         }
316 
317         if (apiOnly) {
318             cmd.createArg().setValue("-apionly");
319         }
320 
321         if (recursive) {
322             cmd.createArg().setValue("-R");
323         }
324 
325         if (jdkinternals) {
326             cmd.createArg().setValue("-jdkinternals");
327         }
328     }
329 
330     protected Set<Path> getDependenciesToAnalyze(boolean includeClasspath)
331             throws DependencyResolutionRequiredException {
332         Set<Path> jdepsClasses = new LinkedHashSet<>();
333 
334         jdepsClasses.add(Paths.get(getClassesDirectory()));
335 
336         if (includeClasspath) {
337             jdepsClasses.addAll(getClassPath());
338         }
339 
340         if (dependenciesToAnalyzeIncludes != null) {
341             MatchPatterns includes = MatchPatterns.from(dependenciesToAnalyzeIncludes);
342 
343             MatchPatterns excludes;
344             if (dependenciesToAnalyzeExcludes != null) {
345                 excludes = MatchPatterns.from(dependenciesToAnalyzeExcludes);
346             } else {
347                 excludes = MatchPatterns.from(Collections.<String>emptyList());
348             }
349 
350             for (Artifact artifact : project.getArtifacts()) {
351                 String versionlessKey = ArtifactUtils.versionlessKey(artifact);
352 
353                 if (includes.matchesPatternStart(versionlessKey, true)
354                         && !excludes.matchesPatternStart(versionlessKey, true)) {
355                     jdepsClasses.add(artifact.getFile().toPath());
356                 }
357             }
358         }
359 
360         return jdepsClasses;
361     }
362 
363     protected void addJDepsClasses(Commandline cmd, Set<Path> dependenciesToAnalyze) {
364         // <classes> can be a pathname to a .class file, a directory, a JAR file, or a fully-qualified class name.
365         for (Path dependencyToAnalyze : dependenciesToAnalyze) {
366             cmd.createArg().setFile(dependencyToAnalyze.toFile());
367         }
368     }
369 
370     private String getJDepsExecutable() throws IOException {
371         Toolchain tc = getToolchain();
372 
373         String jdepsExecutable = null;
374         if (tc != null) {
375             jdepsExecutable = tc.findTool("jdeps");
376         }
377 
378         String jdepsCommand = "jdeps" + (SystemUtils.IS_OS_WINDOWS ? ".exe" : "");
379 
380         File jdepsExe;
381 
382         if (StringUtils.isNotEmpty(jdepsExecutable)) {
383             jdepsExe = new File(jdepsExecutable);
384 
385             if (jdepsExe.isDirectory()) {
386                 jdepsExe = new File(jdepsExe, jdepsCommand);
387             }
388 
389             if (SystemUtils.IS_OS_WINDOWS && jdepsExe.getName().indexOf('.') < 0) {
390                 jdepsExe = new File(jdepsExe.getPath() + ".exe");
391             }
392 
393             if (!jdepsExe.isFile()) {
394                 throw new IOException("The jdeps executable '" + jdepsExe + "' doesn't exist or is not a file.");
395             }
396             return jdepsExe.getAbsolutePath();
397         }
398 
399         jdepsExe = new File(SystemUtils.getJavaHome() + File.separator + ".." + File.separator + "sh", jdepsCommand);
400 
401         // ----------------------------------------------------------------------
402         // Try to find jdepsExe from JAVA_HOME environment variable
403         // ----------------------------------------------------------------------
404         Properties env = CommandLineUtils.getSystemEnvVars();
405         if (!jdepsExe.exists() || !jdepsExe.isFile()) {
406             String javaHome = env.getProperty("JAVA_HOME");
407             if (!StringUtils.isEmpty(javaHome)) {
408                 if ((!new File(javaHome).getCanonicalFile().exists())
409                         || (new File(javaHome).getCanonicalFile().isFile())) {
410                     throw new IOException("The environment variable JAVA_HOME=" + javaHome
411                             + " doesn't exist or is not a valid directory.");
412                 }
413 
414                 jdepsExe = new File(javaHome + File.separator + "bin", jdepsCommand);
415             }
416         }
417 
418         if (!jdepsExe.getCanonicalFile().exists()
419                 || !jdepsExe.getCanonicalFile().isFile()) {
420             // ----------------------------------------------------------------------
421             // Try to find jdepsExe from PATH environment variable
422             // ----------------------------------------------------------------------
423             String path = env.getProperty("PATH");
424             if (path == null) {
425                 path = env.getProperty("Path");
426             }
427             if (path == null) {
428                 path = env.getProperty("path");
429             }
430             if (path != null) {
431                 String[] pathDirs = path.split(File.pathSeparator);
432                 for (String pathDir : pathDirs) {
433                     if (StringUtils.isBlank(pathDir)) {
434                         continue;
435                     }
436                     File pathJdepsExe = new File(pathDir, jdepsCommand);
437                     File canonicalPathJdepsExe = pathJdepsExe.getCanonicalFile();
438                     if (canonicalPathJdepsExe.exists()
439                             && canonicalPathJdepsExe.isFile()
440                             && canonicalPathJdepsExe.canExecute()) {
441                         return canonicalPathJdepsExe.getAbsolutePath();
442                     }
443                 }
444             }
445 
446             throw new IOException(
447                     "Unable to locate the jdeps executable. Verify that JAVA_HOME is set correctly or ensure that jdeps is available on the system PATH.");
448         }
449 
450         if (!jdepsExe.canExecute()) {
451             throw new IOException("The jdeps executable '" + jdepsExe + "' is not executable.");
452         }
453         return jdepsExe.getAbsolutePath();
454     }
455 
456     private void executeJDepsCommandLine(
457             Commandline cmd, File jOutputDirectory, CommandLineUtils.StringStreamConsumer consumer)
458             throws MojoExecutionException {
459         if (getLog().isDebugEnabled()) {
460             // no quoted arguments
461             getLog().debug("Executing: "
462                     + CommandLineUtils.toString(cmd.getCommandline()).replaceAll("'", ""));
463         }
464 
465         CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer() {
466             @Override
467             public void consumeLine(String line) {
468                 if (!line.startsWith("Picked up JAVA_TOOL_OPTIONS:")) {
469                     super.consumeLine(line);
470                 }
471             }
472         };
473         CommandLineUtils.StringStreamConsumer out;
474         if (consumer != null) {
475             out = consumer;
476         } else {
477             out = new CommandLineUtils.StringStreamConsumer();
478         }
479 
480         try {
481             int exitCode = CommandLineUtils.executeCommandLine(cmd, out, err);
482 
483             String output = (StringUtils.isEmpty(out.getOutput())
484                     ? null
485                     : '\n' + out.getOutput().trim());
486 
487             if (exitCode != 0) {
488                 if (StringUtils.isNotEmpty(output)) {
489                     getLog().info(output);
490                 }
491 
492                 StringBuilder msg = new StringBuilder("\nExit code: ");
493                 msg.append(exitCode);
494                 if (StringUtils.isNotEmpty(err.getOutput())) {
495                     msg.append(" - ").append(err.getOutput());
496                 }
497                 msg.append('\n');
498                 msg.append("Command line was: ").append(cmd).append('\n').append('\n');
499 
500                 throw new MojoExecutionException(msg.toString());
501             }
502 
503             if (StringUtils.isNotEmpty(output)) {
504                 getLog().info(output);
505             }
506         } catch (CommandLineException e) {
507             throw new MojoExecutionException("Unable to execute jdeps command: " + e.getMessage(), e);
508         }
509 
510         // ----------------------------------------------------------------------
511         // Handle JDeps warnings
512         // ----------------------------------------------------------------------
513 
514         if (StringUtils.isNotEmpty(err.getOutput()) && getLog().isWarnEnabled()) {
515             getLog().warn("JDeps Warnings");
516 
517             StringTokenizer token = new StringTokenizer(err.getOutput(), "\n");
518             while (token.hasMoreTokens()) {
519                 String current = token.nextToken().trim();
520 
521                 getLog().warn(current);
522             }
523         }
524     }
525 
526     private Toolchain getToolchain() {
527         Toolchain tc = null;
528         if (toolchainManager != null) {
529             tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
530 
531             if (tc == null) {
532                 // Maven 3.2.6 has plugin execution scoped Toolchain Support
533                 try {
534                     Method getToolchainsMethod = toolchainManager
535                             .getClass()
536                             .getMethod("getToolchains", MavenSession.class, String.class, Map.class);
537 
538                     @SuppressWarnings("unchecked")
539                     List<Toolchain> tcs = (List<Toolchain>) getToolchainsMethod.invoke(
540                             toolchainManager, session, "jdk", Collections.singletonMap("version", "[1.8,)"));
541 
542                     if (tcs != null && !tcs.isEmpty()) {
543                         // pick up latest, jdeps of JDK9 has more options compared to JDK8
544                         tc = tcs.get(tcs.size() - 1);
545                     }
546                 } catch (NoSuchMethodException
547                         | SecurityException
548                         | IllegalAccessException
549                         | IllegalArgumentException
550                         | InvocationTargetException e) {
551                     // ignore
552                 }
553             }
554         }
555 
556         return tc;
557     }
558 
559     protected boolean isFailOnWarning() {
560         return failOnWarning;
561     }
562 
563     protected abstract String getClassesDirectory();
564 
565     protected abstract Collection<Path> getClassPath() throws DependencyResolutionRequiredException;
566 }