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             usedUndeclaredArtifactsWithClasses.keySet().retainAll(usedUndeclaredArtifacts);
103 
104             Set<Artifact> unusedDeclaredArtifacts = new LinkedHashSet<>(declaredArtifacts);
105             unusedDeclaredArtifacts = removeAll(unusedDeclaredArtifacts, usedArtifacts.keySet());
106 
107             Set<Artifact> testArtifactsWithNonTestScope = getTestArtifactsWithNonTestScope(testOnlyArtifacts);
108 
109             return new ProjectDependencyAnalysis(
110                     usedDeclaredArtifactsWithClasses, usedUndeclaredArtifactsWithClasses,
111                     unusedDeclaredArtifacts, testArtifactsWithNonTestScope);
112         } catch (IOException exception) {
113             throw new ProjectDependencyAnalyzerException("Cannot analyze dependencies", exception);
114         }
115     }
116 
117     /**
118      * This method defines a new way to remove the artifacts by using the conflict id. We don't care about the version
119      * here because there can be only 1 for a given artifact anyway.
120      *
121      * @param start  initial set
122      * @param remove set to exclude
123      * @return set with remove excluded
124      */
125     private static Set<Artifact> removeAll(Set<Artifact> start, Set<Artifact> remove) {
126         Set<Artifact> results = new LinkedHashSet<>(start.size());
127 
128         for (Artifact artifact : start) {
129             boolean found = false;
130 
131             for (Artifact artifact2 : remove) {
132                 if (artifact.getDependencyConflictId().equals(artifact2.getDependencyConflictId())) {
133                     found = true;
134                     break;
135                 }
136             }
137 
138             if (!found) {
139                 results.add(artifact);
140             }
141         }
142 
143         return results;
144     }
145 
146     private static Set<Artifact> getTestArtifactsWithNonTestScope(Set<Artifact> testOnlyArtifacts) {
147         Set<Artifact> nonTestScopeArtifacts = new LinkedHashSet<>();
148 
149         for (Artifact artifact : testOnlyArtifacts) {
150             if (artifact.getScope().equals("compile")) {
151                 nonTestScopeArtifacts.add(artifact);
152             }
153         }
154 
155         return nonTestScopeArtifacts;
156     }
157 
158     protected Map<Artifact, Set<String>> buildArtifactClassMap(MavenProject project, ClassesPatterns excludedClasses)
159             throws IOException {
160         Map<Artifact, Set<String>> artifactClassMap = new LinkedHashMap<>();
161 
162         Set<Artifact> dependencyArtifacts = project.getArtifacts();
163 
164         for (Artifact artifact : dependencyArtifacts) {
165             File file = artifact.getFile();
166 
167             if (file != null && file.getName().endsWith(".jar")) {
168                 // optimized solution for the jar case
169 
170                 try (JarFile jarFile = new JarFile(file)) {
171                     Enumeration<JarEntry> jarEntries = jarFile.entries();
172 
173                     Set<String> classes = new HashSet<>();
174 
175                     while (jarEntries.hasMoreElements()) {
176                         String entry = jarEntries.nextElement().getName();
177                         if (entry.endsWith(".class")) {
178                             String className = entry.replace('/', '.');
179                             className = className.substring(0, className.length() - ".class".length());
180                             if (!excludedClasses.isMatch(className)) {
181                                 classes.add(className);
182                             }
183                         }
184                     }
185 
186                     artifactClassMap.put(artifact, classes);
187                 }
188             } else if (file != null && file.isDirectory()) {
189                 URL url = file.toURI().toURL();
190                 Set<String> classes = classAnalyzer.analyze(url, excludedClasses);
191 
192                 artifactClassMap.put(artifact, classes);
193             }
194         }
195 
196         return artifactClassMap;
197     }
198 
199     private static Set<DependencyUsage> buildTestOnlyDependencyClasses(
200             Set<DependencyUsage> mainDependencyClasses, Set<DependencyUsage> testDependencyClasses) {
201         Set<DependencyUsage> testOnlyDependencyClasses = new HashSet<>(testDependencyClasses);
202         Set<String> mainDepClassNames = mainDependencyClasses.stream()
203                 .map(DependencyUsage::getDependencyClass)
204                 .collect(Collectors.toSet());
205         testOnlyDependencyClasses.removeIf(u -> mainDepClassNames.contains(u.getDependencyClass()));
206         return testOnlyDependencyClasses;
207     }
208 
209     private Set<DependencyUsage> buildMainDependencyClasses(MavenProject project, ClassesPatterns excludedClasses)
210             throws IOException {
211         String outputDirectory = project.getBuild().getOutputDirectory();
212         return buildDependencyClasses(outputDirectory, excludedClasses);
213     }
214 
215     private Set<DependencyUsage> buildTestDependencyClasses(MavenProject project, ClassesPatterns excludedClasses)
216             throws IOException {
217         String testOutputDirectory = project.getBuild().getTestOutputDirectory();
218         return buildDependencyClasses(testOutputDirectory, excludedClasses);
219     }
220 
221     private Set<DependencyUsage> buildDependencyClasses(String path, ClassesPatterns excludedClasses)
222             throws IOException {
223         URL url = new File(path).toURI().toURL();
224 
225         return dependencyAnalyzer.analyzeUsages(url, excludedClasses);
226     }
227 
228     private static Set<Artifact> buildDeclaredArtifacts(MavenProject project) {
229         Set<Artifact> declaredArtifacts = project.getDependencyArtifacts();
230 
231         if (declaredArtifacts == null) {
232             declaredArtifacts = Collections.emptySet();
233         }
234 
235         return declaredArtifacts;
236     }
237 
238     private static Map<Artifact, Set<DependencyUsage>> buildUsedArtifacts(
239             Map<Artifact, Set<String>> artifactClassMap, Set<DependencyUsage> dependencyClasses) {
240         Map<Artifact, Set<DependencyUsage>> usedArtifacts = new HashMap<>();
241 
242         for (DependencyUsage classUsage : dependencyClasses) {
243             Artifact artifact = findArtifactForClassName(artifactClassMap, classUsage.getDependencyClass());
244 
245             if (artifact != null) {
246                 Set<DependencyUsage> classesFromArtifact = usedArtifacts.get(artifact);
247                 if (classesFromArtifact == null) {
248                     classesFromArtifact = new HashSet<>();
249                     usedArtifacts.put(artifact, classesFromArtifact);
250                 }
251                 classesFromArtifact.add(classUsage);
252             }
253         }
254 
255         return usedArtifacts;
256     }
257 
258     private static Artifact findArtifactForClassName(Map<Artifact, Set<String>> artifactClassMap, String className) {
259         for (Map.Entry<Artifact, Set<String>> entry : artifactClassMap.entrySet()) {
260             if (entry.getValue().contains(className)) {
261                 return entry.getKey();
262             }
263         }
264 
265         return null;
266     }
267 }