View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package org.basepom.mojo.duplicatefinder.artifact;
16  
17  import static com.google.common.base.Preconditions.checkNotNull;
18  import static com.google.common.base.Preconditions.checkState;
19  import static java.lang.String.format;
20  import static org.basepom.mojo.duplicatefinder.artifact.ArtifactHelper.getOutputDirectory;
21  import static org.basepom.mojo.duplicatefinder.artifact.ArtifactHelper.getTestOutputDirectory;
22  import static org.basepom.mojo.duplicatefinder.artifact.ArtifactHelper.isTestArtifact;
23  
24  import org.basepom.mojo.duplicatefinder.ClasspathElement;
25  import org.basepom.mojo.duplicatefinder.ClasspathElement.ClasspathArtifact;
26  import org.basepom.mojo.duplicatefinder.ClasspathElement.ClasspathLocalFolder;
27  
28  import java.io.File;
29  import java.io.IOException;
30  import java.util.Collection;
31  import java.util.HashMap;
32  import java.util.Map;
33  import java.util.Objects;
34  import java.util.Set;
35  
36  import com.google.common.annotations.VisibleForTesting;
37  import com.google.common.base.MoreObjects;
38  import com.google.common.collect.ImmutableMap;
39  import com.google.common.collect.ImmutableMultimap;
40  import com.google.common.collect.ImmutableSet;
41  import com.google.common.collect.ImmutableSortedSet;
42  import com.google.common.collect.Multimap;
43  import com.google.common.collect.MultimapBuilder;
44  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
45  import org.apache.maven.artifact.Artifact;
46  import org.apache.maven.artifact.DefaultArtifact;
47  import org.apache.maven.artifact.DependencyResolutionRequiredException;
48  import org.apache.maven.artifact.versioning.VersionRange;
49  import org.apache.maven.project.MavenProject;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  /**
54   * Resolves artifact references from the project into local and repository files and folders.
55   * <p>
56   * Only manages the dependencies because the main project can have multiple (two) folders for the project. This is not supported by this resolver.
57   */
58  @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
59  public class ArtifactFileResolver {
60  
61      private static final Logger LOG = LoggerFactory.getLogger(ArtifactFileResolver.class);
62  
63      // A BiMultimap would come in really handy here...
64      private final Multimap<File, Artifact> localFileArtifactCache;
65      private final Map<Artifact, File> localArtifactFileCache;
66  
67      private final Map<Artifact, File> repoArtifactCache;
68  
69      // the artifact cache can not be a bimap, because for system artifacts, it is possible that multiple
70      // maven coordinates point to the same file.
71      private final Multimap<File, Artifact> repoFileCache = MultimapBuilder
72              .hashKeys()
73              .hashSetValues()
74              .build();
75      private final boolean preferLocal;
76  
77      public ArtifactFileResolver(final MavenProject project,
78              final boolean preferLocal) throws DependencyResolutionRequiredException, IOException {
79          checkNotNull(project, "project is null");
80          this.preferLocal = preferLocal;
81  
82          // This needs to be a multimap, because it is possible by jiggling with classifiers that a local project
83          // (with local folders) does map to multiple artifacts and therefore the file <-> artifact relation is not
84          // 1:1 but 1:n. As found in https://github.com/basepom/duplicate-finder-maven-plugin/issues/10
85          ImmutableMultimap.Builder<File, Artifact> localFileArtifactCacheBuilder = ImmutableMultimap.builder();
86  
87          // This can not be an immutable map builder, because the map is used for looking up while it is built up.
88          this.repoArtifactCache = new HashMap<>(project.getArtifacts().size());
89  
90          for (final Artifact artifact : project.getArtifacts()) {
91              final File repoPath = artifact.getFile().getCanonicalFile();
92              final Artifact canonicalizedArtifact = ArtifactFileResolver.canonicalizeArtifact(artifact);
93  
94              checkState(repoPath.exists(), "Repository Path '%s' does not exist.", repoPath);
95              final File oldFile = repoArtifactCache.put(canonicalizedArtifact, repoPath);
96              checkState(oldFile == null || oldFile.equals(repoPath), "Already encountered a file for %s: %s", canonicalizedArtifact, oldFile);
97              repoFileCache.put(repoPath, canonicalizedArtifact);
98          }
99  
100         for (final MavenProject referencedProject : project.getProjectReferences().values()) {
101             // referenced projects only have GAV coordinates but no scope.
102             final Set<Artifact> repoArtifacts = findRepoArtifacts(referencedProject, repoArtifactCache);
103 
104             // This can happen if another sub-project in the reactor is e.g. used as a compiler plugin dependency.
105             // In that case, the dependency will show up as a referenced project but not in the artifacts list from the project.
106             // Fix up straight from the referenced project.
107             if (repoArtifacts.isEmpty()) {
108                 LOG.debug(
109                         format("Found project reference to %s but no repo reference, probably used in a plugin dependency.", referencedProject.getArtifact()));
110             }
111 
112             for (final Artifact artifact : repoArtifacts) {
113 
114                 final File outputDir = isTestArtifact(artifact) ? getTestOutputDirectory(referencedProject) : getOutputDirectory(referencedProject);
115 
116                 if (outputDir.exists()) {
117                     localFileArtifactCacheBuilder.put(outputDir, artifact);
118                 }
119             }
120         }
121 
122         this.localFileArtifactCache = localFileArtifactCacheBuilder.build();
123 
124         // Flip the File -> Artifact multimap. This also acts as a sanity check because no artifact
125         // must be present more than one and the Map.Builder will choke if a key is around more than
126         // once.
127         ImmutableMap.Builder<Artifact, File> localArtifactFileCacheBuilder = ImmutableMap.builder();
128         for (Map.Entry<File, Artifact> entry : localFileArtifactCache.entries()) {
129             localArtifactFileCacheBuilder.put(entry.getValue(), entry.getKey());
130         }
131 
132         this.localArtifactFileCache = localArtifactFileCacheBuilder.build();
133     }
134 
135     public ImmutableMultimap<File, Artifact> resolveArtifactsForScopes(final Set<String> scopes) {
136         checkNotNull(scopes, "scopes is null");
137 
138         final ImmutableMultimap.Builder<File, Artifact> inScopeBuilder = ImmutableMultimap.builder();
139         for (final Artifact artifact : listArtifacts()) {
140             if (artifact.getArtifactHandler().isAddedToClasspath()) {
141                 if (scopes.isEmpty() || scopes.contains(artifact.getScope())) {
142                     final File file = resolveFileForArtifact(artifact);
143                     checkState(file != null, "No file for artifact '%s' found!", artifact);
144                     inScopeBuilder.put(file, artifact);
145                 }
146             }
147         }
148 
149         return inScopeBuilder.build();
150     }
151 
152     public ImmutableSortedSet<ClasspathElement> getClasspathElementsForElements(final Collection<File> elements) {
153         final ImmutableSortedSet.Builder<ClasspathElement> builder = ImmutableSortedSet.naturalOrder();
154 
155         for (final File element : elements) {
156             resolveClasspathElementsForFile(element, builder);
157         }
158         return builder.build();
159     }
160 
161     private void resolveClasspathElementsForFile(final File file, ImmutableSet.Builder<ClasspathElement> builder) {
162         checkNotNull(file, "file is null");
163 
164         if (preferLocal && localFileArtifactCache.containsKey(file)) {
165             for (Artifact artifact : localFileArtifactCache.get(file)) {
166                 builder.add(new ClasspathArtifact(artifact));
167             }
168             return;
169         }
170 
171         if (repoFileCache.containsKey(file)) {
172             for (Artifact artifact : repoFileCache.get(file)) {
173                 builder.add(new ClasspathArtifact(artifact));
174             }
175             return;
176         }
177 
178         if (localFileArtifactCache.containsKey(file)) {
179             for (Artifact artifact : localFileArtifactCache.get(file)) {
180                 builder.add(new ClasspathArtifact(artifact));
181             }
182             return;
183         }
184 
185         builder.add(new ClasspathLocalFolder(file));
186     }
187 
188     private File resolveFileForArtifact(final Artifact artifact) {
189         checkNotNull(artifact, "artifact is null");
190 
191         if (preferLocal && localArtifactFileCache.containsKey(artifact)) {
192             return localArtifactFileCache.get(artifact);
193         }
194 
195         if (repoArtifactCache.containsKey(artifact)) {
196             return repoArtifactCache.get(artifact);
197         }
198 
199         return localArtifactFileCache.get(artifact);
200     }
201 
202     @VisibleForTesting
203     static DefaultArtifact canonicalizeArtifact(final Artifact artifact) {
204         final VersionRange versionRange =
205                 artifact.getVersionRange() == null ? VersionRange.createFromVersion(artifact.getVersion()) : artifact.getVersionRange();
206         String type = MoreObjects.firstNonNull(artifact.getType(), "jar");
207         String classifier = artifact.getClassifier();
208 
209         if ("test-jar".equals(type) && (classifier == null || "tests".equals(classifier))) {
210             type = "jar";
211             classifier = "tests";
212         }
213 
214         return new DefaultArtifact(artifact.getGroupId(),
215                 artifact.getArtifactId(),
216                 versionRange,
217                 artifact.getScope(),
218                 type,
219                 classifier,
220                 artifact.getArtifactHandler(),
221                 artifact.isOptional());
222     }
223 
224     private Set<Artifact> listArtifacts() {
225         return ImmutableSet.<Artifact>builder().addAll(localArtifactFileCache.keySet()).addAll(repoArtifactCache.keySet()).build();
226     }
227 
228     private static Set<Artifact> findRepoArtifacts(final MavenProject project, final Map<Artifact, File> repoArtifactCache) {
229         final ImmutableSet.Builder<Artifact> builder = ImmutableSet.builder();
230 
231         for (final Artifact artifact : repoArtifactCache.keySet()) {
232             if (Objects.equals(project.getArtifact().getGroupId(), artifact.getGroupId())
233                     && Objects.equals(project.getArtifact().getArtifactId(), artifact.getArtifactId())
234                     && Objects.equals(project.getArtifact().getBaseVersion(), artifact.getBaseVersion())) {
235                 builder.add(artifact);
236             }
237         }
238         return builder.build();
239     }
240 }