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