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