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