View Javadoc
1   package org.apache.maven.shared.dependency.analyzer;
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.net.URL;
25  import java.util.Collections;
26  import java.util.Enumeration;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.LinkedHashMap;
30  import java.util.LinkedHashSet;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.jar.JarEntry;
34  import java.util.jar.JarFile;
35  
36  import org.apache.maven.artifact.Artifact;
37  import org.apache.maven.project.MavenProject;
38  import org.codehaus.plexus.component.annotations.Component;
39  import org.codehaus.plexus.component.annotations.Requirement;
40  
41  /**
42   * <p>DefaultProjectDependencyAnalyzer class.</p>
43   *
44   * @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a>
45   */
46  @Component( role = ProjectDependencyAnalyzer.class )
47  public class DefaultProjectDependencyAnalyzer
48      implements ProjectDependencyAnalyzer
49  {
50      /**
51       * ClassAnalyzer
52       */
53      @Requirement
54      private ClassAnalyzer classAnalyzer;
55  
56      /**
57       * DependencyAnalyzer
58       */
59      @Requirement
60      private DependencyAnalyzer dependencyAnalyzer;
61  
62      /** {@inheritDoc} */
63      public ProjectDependencyAnalysis analyze( MavenProject project )
64          throws ProjectDependencyAnalyzerException
65      {
66          try
67          {
68              Map<Artifact, Set<String>> artifactClassMap = buildArtifactClassMap( project );
69  
70              Set<String> dependencyClasses = buildDependencyClasses( project );
71              Set<String> mainDependencyClasses = buildMainDependencyClasses( project );
72  
73              Set<String> testOnlyDependencyClasses = buildTestDependencyClasses( project );
74  
75              Set<Artifact> declaredArtifacts = buildDeclaredArtifacts( project );
76  
77              Map<Artifact, Set<String>> usedArtifacts = buildUsedArtifacts( artifactClassMap, dependencyClasses );
78              Set<Artifact>  mainUsedArtifacts = buildUsedArtifacts( artifactClassMap, mainDependencyClasses ).keySet();
79  
80              Set<Artifact>  testArtifacts = buildUsedArtifacts( artifactClassMap, testOnlyDependencyClasses ).keySet();
81              Set<Artifact> testOnlyArtifacts = removeAll( testArtifacts, mainUsedArtifacts );
82  
83              Set<Artifact> usedDeclaredArtifacts = new LinkedHashSet<>( declaredArtifacts );
84              usedDeclaredArtifacts.retainAll( usedArtifacts.keySet() );
85  
86              Map<Artifact, Set<String>> usedUndeclaredArtifactsWithClasses = new LinkedHashMap<>( usedArtifacts );
87              Set<Artifact> usedUndeclaredArtifacts = removeAll(
88                      usedUndeclaredArtifactsWithClasses.keySet(), declaredArtifacts );
89              usedUndeclaredArtifactsWithClasses.keySet().retainAll( usedUndeclaredArtifacts );
90  
91              Set<Artifact> unusedDeclaredArtifacts = new LinkedHashSet<>( declaredArtifacts );
92              unusedDeclaredArtifacts = removeAll( unusedDeclaredArtifacts, usedArtifacts.keySet() );
93  
94              Set<Artifact> testArtifactsWithNonTestScope = getTestArtifactsWithNonTestScope( testOnlyArtifacts );
95  
96              return new ProjectDependencyAnalysis( usedDeclaredArtifacts, usedUndeclaredArtifactsWithClasses,
97                                                    unusedDeclaredArtifacts, testArtifactsWithNonTestScope );
98          }
99          catch ( IOException exception )
100         {
101             throw new ProjectDependencyAnalyzerException( "Cannot analyze dependencies", exception );
102         }
103     }
104 
105     /**
106      * This method defines a new way to remove the artifacts by using the conflict id. We don't care about the version
107      * here because there can be only 1 for a given artifact anyway.
108      * 
109      * @param start initial set
110      * @param remove set to exclude
111      * @return set with remove excluded
112      */
113     private Set<Artifact> removeAll( Set<Artifact> start, Set<Artifact> remove )
114     {
115         Set<Artifact> results = new LinkedHashSet<>( start.size() );
116 
117         for ( Artifact artifact : start )
118         {
119             boolean found = false;
120 
121             for ( Artifact artifact2 : remove )
122             {
123                 if ( artifact.getDependencyConflictId().equals( artifact2.getDependencyConflictId() ) )
124                 {
125                     found = true;
126                     break;
127                 }
128             }
129 
130             if ( !found )
131             {
132                 results.add( artifact );
133             }
134         }
135 
136         return results;
137     }
138 
139     private Set<Artifact> getTestArtifactsWithNonTestScope( Set<Artifact> testOnlyArtifacts )
140     {
141         Set<Artifact> nonTestScopeArtifacts = new LinkedHashSet<>();
142 
143         for ( Artifact artifact : testOnlyArtifacts )
144         {
145             if ( artifact.getScope().equals( "compile" ) )
146             {
147                 nonTestScopeArtifacts.add( artifact );
148             }
149         }
150 
151         return nonTestScopeArtifacts;
152     }
153 
154     private Map<Artifact, Set<String>> buildArtifactClassMap( MavenProject project )
155         throws IOException
156     {
157         Map<Artifact, Set<String>> artifactClassMap = new LinkedHashMap<>();
158 
159         Set<Artifact> dependencyArtifacts = project.getArtifacts();
160 
161         for ( Artifact artifact : dependencyArtifacts )
162         {
163             File file = artifact.getFile();
164 
165             if ( file != null && file.getName().endsWith( ".jar" ) )
166             {
167                 // optimized solution for the jar case
168 
169                 try ( JarFile jarFile = new JarFile( file ) )
170                 {
171                     Enumeration<JarEntry> jarEntries = jarFile.entries();
172 
173                     Set<String> classes = new HashSet<>();
174 
175                     while ( jarEntries.hasMoreElements() )
176                     {
177                         String entry = jarEntries.nextElement().getName();
178                         if ( entry.endsWith( ".class" ) )
179                         {
180                             String className = entry.replace( '/', '.' );
181                             className = className.substring( 0, className.length() - ".class".length() );
182                             classes.add( className );
183                         }
184                     }
185 
186                     artifactClassMap.put( artifact, classes );
187                 }
188             }
189             else if ( file != null && file.isDirectory() )
190             {
191                 URL url = file.toURI().toURL();
192                 Set<String> classes = classAnalyzer.analyze( url );
193 
194                 artifactClassMap.put( artifact, classes );
195             }
196         }
197 
198         return artifactClassMap;
199     }
200 
201     private Set<String> buildTestDependencyClasses( MavenProject project ) throws IOException
202     {
203         Set<String> testOnlyDependencyClasses = new HashSet<>();
204 
205         String outputDirectory = project.getBuild().getOutputDirectory();
206         Set<String> nonTestDependencyClasses = new HashSet<>( buildDependencyClasses( outputDirectory ) );
207 
208         String testOutputDirectory = project.getBuild().getTestOutputDirectory();
209         Set<String> testDependencyClasses = new HashSet<>( buildDependencyClasses( testOutputDirectory ) );
210 
211         for ( String testString : testDependencyClasses )
212         {
213             if ( !nonTestDependencyClasses.contains( testString ) )
214             {
215                 testOnlyDependencyClasses.add( testString );
216             }
217         }
218 
219         return testOnlyDependencyClasses;
220     }
221 
222     private Set<String> buildDependencyClasses( MavenProject project )
223         throws IOException
224     {
225 
226         String outputDirectory = project.getBuild().getOutputDirectory();
227         Set<String> dependencyClasses = new HashSet<>( buildDependencyClasses( outputDirectory ) );
228 
229         String testOutputDirectory = project.getBuild().getTestOutputDirectory();
230         dependencyClasses.addAll( buildDependencyClasses( testOutputDirectory ) );
231 
232         return dependencyClasses;
233     }
234     
235     private Set<String> buildMainDependencyClasses( MavenProject project )
236                     throws IOException
237     {
238 
239         String outputDirectory = project.getBuild().getOutputDirectory();
240         Set<String> dependencyClasses = new HashSet<>( buildDependencyClasses( outputDirectory ) );
241 
242         return dependencyClasses;
243     }
244 
245     private Set<String> buildDependencyClasses( String path )
246         throws IOException
247     {
248         URL url = new File( path ).toURI().toURL();
249 
250         return dependencyAnalyzer.analyze( url );
251     }
252 
253     private Set<Artifact> buildDeclaredArtifacts( MavenProject project )
254     {
255         Set<Artifact> declaredArtifacts = project.getDependencyArtifacts();
256 
257         if ( declaredArtifacts == null )
258         {
259             declaredArtifacts = Collections.emptySet();
260         }
261 
262         return declaredArtifacts;
263     }
264 
265     private Map<Artifact, Set<String>> buildUsedArtifacts( Map<Artifact, Set<String>> artifactClassMap,
266                                               Set<String> dependencyClasses )
267     {
268         Map<Artifact, Set<String>> usedArtifacts = new HashMap<>();
269 
270         for ( String className : dependencyClasses )
271         {
272             Artifact artifact = findArtifactForClassName( artifactClassMap, className );
273 
274             if ( artifact != null )
275             {
276                 Set<String> classesFromArtifact = usedArtifacts.get( artifact );
277                 if ( classesFromArtifact == null )
278                 {
279                     classesFromArtifact = new HashSet<String>();
280                     usedArtifacts.put( artifact, classesFromArtifact );
281                 }
282                 classesFromArtifact.add( className );
283             }
284         }
285 
286         return usedArtifacts;
287     }
288 
289     private Artifact findArtifactForClassName( Map<Artifact, Set<String>> artifactClassMap, String className )
290     {
291         for ( Map.Entry<Artifact, Set<String>> entry : artifactClassMap.entrySet() )
292         {
293             if ( entry.getValue().contains( className ) )
294             {
295                 return entry.getKey();
296             }
297         }
298 
299         return null;
300     }
301 }