1
2
3
4
5
6
7
8
9
10
11
12
13
14 package org.basepom.mojo.duplicatefinder.classpath;
15
16 import java.io.File;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.Map.Entry;
23 import java.util.Optional;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ConcurrentMap;
26 import java.util.function.Predicate;
27 import java.util.regex.Pattern;
28 import java.util.regex.PatternSyntaxException;
29 import java.util.zip.ZipEntry;
30 import java.util.zip.ZipInputStream;
31
32 import javax.lang.model.SourceVersion;
33
34 import com.google.common.annotations.VisibleForTesting;
35 import com.google.common.base.MoreObjects;
36 import com.google.common.base.Splitter;
37 import com.google.common.collect.ImmutableList;
38 import com.google.common.collect.ImmutableList.Builder;
39 import com.google.common.collect.ImmutableMap;
40 import com.google.common.collect.ImmutableMultimap;
41 import com.google.common.collect.Multimap;
42 import com.google.common.collect.MultimapBuilder;
43 import com.google.common.io.Files;
44 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
45 import org.apache.maven.artifact.Artifact;
46 import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
47 import org.apache.maven.plugin.MojoExecutionException;
48 import org.apache.maven.project.MavenProject;
49 import org.basepom.mojo.duplicatefinder.ConflictType;
50 import org.basepom.mojo.duplicatefinder.artifact.MavenCoordinates;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import static java.lang.String.format;
55
56 import static com.google.common.base.Preconditions.checkNotNull;
57 import static com.google.common.base.Preconditions.checkState;
58
59 @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
60 public class ClasspathDescriptor {
61
62 private static final Logger LOG = LoggerFactory.getLogger(ClasspathDescriptor.class);
63
64 private static final MatchPatternPredicate DEFAULT_IGNORED_RESOURCES_PREDICATE = new MatchPatternPredicate(Arrays.asList(
65
66 "^META-INF/.*",
67 "^OSGI-INF/.*",
68
69 "^licenses/.*",
70
71 ".*license(\\.txt)?$",
72 ".*notice(\\.txt)?$",
73 ".*readme(\\.txt)?$",
74 ".*changelog(\\.txt)?$",
75 ".*third-party(\\.txt)?$",
76
77 ".*package\\.html$",
78 ".*overview\\.html$"));
79
80 @VisibleForTesting
81 static final MatchPatternPredicate DEFAULT_IGNORED_CLASS_PREDICATE = new MatchPatternPredicate(Arrays.asList(
82
83 "^(.*\\.)?.*\\$.*$",
84 "^(.*\\.)?package-info$",
85 "^(.*\\.)?module-info$"
86 ));
87
88 private static final MatchPatternPredicate DEFAULT_IGNORED_LOCAL_DIRECTORIES = new MatchPatternPredicate(Arrays.asList(
89 "^.git$",
90 "^.svn$",
91 "^.hg$",
92 "^.bzr$"));
93
94
95
96
97
98 private static final ConcurrentMap<File, ClasspathCacheElement> CACHED_BY_FILE = new ConcurrentHashMap<>();
99
100 private final Multimap<String, File> classesWithElements = MultimapBuilder.treeKeys().hashSetValues().build();
101 private final Multimap<String, File> resourcesWithElements = MultimapBuilder.treeKeys().hashSetValues().build();
102
103 private final Predicate<String> resourcesPredicate;
104 private final Predicate<String> classPredicate;
105
106 private final ImmutableList<Pattern> ignoredResourcePatterns;
107 private final ImmutableList<Pattern> ignoredClassPatterns;
108
109 public static ClasspathDescriptor createClasspathDescriptor(final MavenProject project,
110 final Multimap<File, Artifact> fileToArtifactMap,
111 final Collection<String> ignoredResourcePatterns,
112 final Collection<String> ignoredClassPatterns,
113 final Collection<MavenCoordinates> ignoredDependencies,
114 final boolean useDefaultResourceIgnoreList,
115 final boolean useDefaultClassIgnoreList,
116 final File[] projectFolders) throws MojoExecutionException, InvalidVersionSpecificationException {
117 checkNotNull(project, "project is null");
118 checkNotNull(fileToArtifactMap, "fileToArtifactMap is null");
119 checkNotNull(ignoredResourcePatterns, "ignoredResourcePatterns is null");
120 checkNotNull(ignoredClassPatterns, "ignoredClassPatterns is null");
121 checkNotNull(ignoredDependencies, "ignoredDependencies is null");
122 checkNotNull(projectFolders, "projectFolders is null");
123
124 final ClasspathDescriptor classpathDescriptor = new ClasspathDescriptor(useDefaultResourceIgnoreList, ignoredResourcePatterns,
125 useDefaultClassIgnoreList, ignoredClassPatterns);
126
127 File file = null;
128
129 final MatchArtifactPredicate matchArtifactPredicate = new MatchArtifactPredicate(ignoredDependencies);
130
131 Artifact artifact = null;
132
133 try {
134
135
136
137 for (final Entry<File, Artifact> entry : fileToArtifactMap.entries()) {
138 artifact = entry.getValue();
139 file = entry.getKey();
140
141 if (file.exists()) {
142
143 if (!matchArtifactPredicate.apply(artifact)) {
144 classpathDescriptor.addClasspathElement(file);
145 }
146 } else {
147
148
149
150
151
152 LOG.debug(format("Classpath element '%s' does not exist.", file.getAbsolutePath()));
153 }
154 }
155 } catch (final IOException ex) {
156 throw new MojoExecutionException(format("Error trying to access file '%s' for artifact '%s'", file, artifact), ex);
157 }
158
159 try {
160
161 for (final File projectFile : projectFolders) {
162 file = projectFile;
163 if (projectFile.exists()) {
164 classpathDescriptor.addClasspathElement(file);
165 } else {
166
167 LOG.debug(format("Project folder '%s' does not exist.", file.getAbsolutePath()));
168 }
169 }
170 } catch (final IOException ex) {
171 throw new MojoExecutionException(format("Error trying to access project folder '%s'", file), ex);
172 }
173
174 return classpathDescriptor;
175 }
176
177 private ClasspathDescriptor(final boolean useDefaultResourceIgnoreList,
178 final Collection<String> ignoredResourcePatterns,
179 final boolean useDefaultClassIgnoreList,
180 final Collection<String> ignoredClassPatterns)
181 throws MojoExecutionException {
182 final Builder<Pattern> ignoredResourcePatternsBuilder = ImmutableList.builder();
183
184
185 Predicate<String> resourcesPredicate = s -> false;
186
187
188 if (useDefaultResourceIgnoreList) {
189 resourcesPredicate = resourcesPredicate.or(DEFAULT_IGNORED_RESOURCES_PREDICATE);
190 ignoredResourcePatternsBuilder.addAll(DEFAULT_IGNORED_RESOURCES_PREDICATE.getPatterns());
191 }
192
193 if (!ignoredResourcePatterns.isEmpty()) {
194 try {
195
196 MatchPatternPredicate ignoredResourcesPredicate = new MatchPatternPredicate(ignoredResourcePatterns);
197 resourcesPredicate = resourcesPredicate.or(ignoredResourcesPredicate);
198 ignoredResourcePatternsBuilder.addAll(ignoredResourcesPredicate.getPatterns());
199 } catch (final PatternSyntaxException pse) {
200 throw new MojoExecutionException("Error compiling resourceIgnore pattern: " + pse.getMessage());
201 }
202 }
203
204 this.resourcesPredicate = resourcesPredicate;
205 this.ignoredResourcePatterns = ignoredResourcePatternsBuilder.build();
206
207 final Builder<Pattern> ignoredClassPatternsBuilder = ImmutableList.builder();
208
209
210 Predicate<String> classPredicate = s -> false;
211
212
213 if (useDefaultClassIgnoreList) {
214 classPredicate = classPredicate.or(DEFAULT_IGNORED_CLASS_PREDICATE);
215 ignoredClassPatternsBuilder.addAll(DEFAULT_IGNORED_CLASS_PREDICATE.getPatterns());
216 }
217
218 if (!ignoredClassPatterns.isEmpty()) {
219 try {
220
221 MatchPatternPredicate ignoredPackagePredicate = new MatchPatternPredicate(ignoredClassPatterns);
222 classPredicate = classPredicate.or(ignoredPackagePredicate);
223 ignoredClassPatternsBuilder.addAll(ignoredPackagePredicate.getPatterns());
224 } catch (final PatternSyntaxException pse) {
225 throw new MojoExecutionException("Error compiling classIgnore pattern: " + pse.getMessage());
226 }
227 }
228
229 this.classPredicate = classPredicate;
230 this.ignoredClassPatterns = ignoredClassPatternsBuilder.build();
231 }
232
233 public ImmutableMap<String, Collection<File>> getClasspathElementLocations(final ConflictType type) {
234 checkNotNull(type, "type is null");
235 switch (type) {
236 case CLASS:
237 return ImmutableMultimap.copyOf(classesWithElements).asMap();
238 case RESOURCE:
239 return ImmutableMultimap.copyOf(resourcesWithElements).asMap();
240 default:
241 throw new IllegalStateException("Type '" + type + "' unknown!");
242 }
243 }
244
245 public ImmutableList<Pattern> getIgnoredResourcePatterns() {
246 return ignoredResourcePatterns;
247 }
248
249 public ImmutableList<Pattern> getIgnoredClassPatterns() {
250 return ignoredClassPatterns;
251 }
252
253 public ImmutableList<Pattern> getIgnoredDirectoryPatterns() {
254 return DEFAULT_IGNORED_LOCAL_DIRECTORIES.getPatterns();
255 }
256
257
258 private void addClasspathElement(final File element) throws IOException {
259 checkState(element.exists(), "Path '%s' does not exist!", element.getAbsolutePath());
260
261 ClasspathCacheElement cached = CACHED_BY_FILE.get(element);
262
263 if (cached == null) {
264 final ClasspathCacheElement.Builder cacheBuilder = ClasspathCacheElement.builder(element);
265 if (element.isDirectory()) {
266 addDirectory(cacheBuilder, element, new PackageNameHolder());
267 } else {
268 addArchive(cacheBuilder, element);
269 }
270 final ClasspathCacheElement newCached = cacheBuilder.build();
271 final ClasspathCacheElement oldCached = CACHED_BY_FILE.putIfAbsent(element, newCached);
272 cached = MoreObjects.firstNonNull(oldCached, newCached);
273 } else {
274 LOG.debug(format("Cache hit for '%s'", element.getAbsolutePath()));
275 }
276
277 cached.putResources(resourcesWithElements, resourcesPredicate);
278 cached.putClasses(classesWithElements, classPredicate);
279
280 }
281
282 private void addDirectory(final ClasspathCacheElement.Builder cacheBuilder, final File workDir, final PackageNameHolder packageName) {
283 final File[] files = workDir.listFiles();
284
285 if (files != null) {
286 for (final File file : files) {
287 if (file.isDirectory()) {
288 if (DEFAULT_IGNORED_LOCAL_DIRECTORIES.apply(file.getName())) {
289 LOG.debug(format("Ignoring local directory '%s'", file.getAbsolutePath()));
290 } else {
291 addDirectory(cacheBuilder, file, packageName.getChildPackage(file.getName()));
292 }
293
294 } else if (file.isFile()) {
295 if ("class".equals(Files.getFileExtension(file.getName()))) {
296 final String className = packageName.getQualifiedName(Files.getNameWithoutExtension(file.getName()));
297 cacheBuilder.addClass(className);
298 } else {
299 final String resourcePath = packageName.getQualifiedPath(file.getName());
300 cacheBuilder.addResource(resourcePath);
301 }
302 } else {
303 LOG.warn(format("Ignoring unknown file type for '%s'", file.getAbsolutePath()));
304 }
305 }
306 }
307 }
308
309 private void addArchive(final ClasspathCacheElement.Builder cacheBuilder, final File element) throws IOException {
310
311 try (
312 final InputStream input = element.toURI().toURL().openStream();
313 final ZipInputStream zipInput = new ZipInputStream(input)) {
314
315 ZipEntry entry;
316
317 while ((entry = zipInput.getNextEntry()) != null) {
318 if (!entry.isDirectory()) {
319 final String name = entry.getName();
320 Optional<List<String>> validatedElements = validateClassName(name);
321 if (validatedElements.isPresent()) {
322 List<String> nameElements = validatedElements.get();
323 final PackageNameHolder packageName = new PackageNameHolder(nameElements.subList(0, nameElements.size() - 1));
324 final String className = packageName.getQualifiedName(Files.getNameWithoutExtension(name));
325 cacheBuilder.addClass(className);
326 } else {
327 final String resourcePath = name.replace('\\', File.separatorChar);
328 cacheBuilder.addResource(resourcePath);
329 }
330 }
331 }
332 }
333 }
334
335 @VisibleForTesting
336 static Optional<List<String>> validateClassName(String fullClassPath) {
337 if (fullClassPath == null) {
338 return Optional.empty();
339 }
340
341
342 final List<String> nameElements = ImmutableList.copyOf(Splitter.on("/").splitToList(fullClassPath));
343 if (nameElements.isEmpty()) {
344 LOG.warn(format("ZIP entry '%s' split into empty list!", fullClassPath));
345 return Optional.empty();
346 }
347 String classFileName = nameElements.get(nameElements.size() - 1);
348
349 if (!"class".equals(Files.getFileExtension(classFileName))) {
350 LOG.debug(format("Ignoring %s, %s is not a class file", fullClassPath, classFileName));
351 return Optional.empty();
352 }
353 for (int i = 0; i < nameElements.size() - 1; i++) {
354 if (!SourceVersion.isIdentifier(nameElements.get(i))) {
355 LOG.debug(format("Ignoring %s, %s is not a valid package element", fullClassPath, nameElements.get(i)));
356 return Optional.empty();
357 }
358 }
359
360 String className = Files.getNameWithoutExtension(classFileName);
361 if (!(SourceVersion.isIdentifier(className)
362 || "module-info".equals(className)
363 || "package-info".equals(className))) {
364 LOG.debug(format("Ignoring %s, %s is not a valid class identifier", fullClassPath, className));
365 return Optional.empty();
366 }
367
368 return Optional.of(nameElements);
369 }
370 }