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