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