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.eclipse.aether.internal.impl.collect.bf;
20  
21  import java.io.Closeable;
22  import java.util.HashMap;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.concurrent.atomic.AtomicInteger;
27  
28  import org.eclipse.aether.artifact.Artifact;
29  import org.eclipse.aether.graph.DependencyNode;
30  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  /**
35   * A skipper that determines whether to skip resolving given node during the dependency collection.
36   * Internal helper for {@link BfDependencyCollector}.
37   *
38   * @since 1.8.0
39   */
40  abstract class DependencyResolutionSkipper implements Closeable {
41      /**
42       * Check whether the resolution of current node can be skipped before resolving.
43       *
44       * @param node    Current node
45       * @param parents All parent nodes of current node
46       *
47       * @return {@code true} if the node can be skipped for resolution, {@code false} if resolution required.
48       */
49      abstract boolean skipResolution(DependencyNode node, List<DependencyNode> parents);
50  
51      /**
52       * Cache the resolution result when a node is resolved by {@link BfDependencyCollector) after resolution.
53       *
54       * @param node    Current node
55       * @param parents All parent nodes of current node
56       */
57      abstract void cache(DependencyNode node, List<DependencyNode> parents);
58  
59      /**
60       * Close: Print the skip/resolve status report for all nodes.
61       */
62      @Override
63      public abstract void close();
64  
65      /**
66       * Returns new instance of "default" skipper.
67       *
68       * Note: type is specialized for testing purposes.
69       */
70      public static DefaultDependencyResolutionSkipper defaultSkipper() {
71          return new DefaultDependencyResolutionSkipper();
72      }
73  
74      /**
75       * Returns instance of "never" skipper.
76       */
77      public static DependencyResolutionSkipper neverSkipper() {
78          return NeverDependencyResolutionSkipper.INSTANCE;
79      }
80  
81      /**
82       * NEVER implementation.
83       */
84      private static final class NeverDependencyResolutionSkipper extends DependencyResolutionSkipper {
85          private static final DependencyResolutionSkipper INSTANCE = new NeverDependencyResolutionSkipper();
86  
87          @Override
88          public boolean skipResolution(DependencyNode node, List<DependencyNode> parents) {
89              return false;
90          }
91  
92          @Override
93          public void cache(DependencyNode node, List<DependencyNode> parents) {}
94  
95          @Override
96          public void close() {}
97      }
98  
99      /**
100      * Visible for testing.
101      */
102     static final class DefaultDependencyResolutionSkipper extends DependencyResolutionSkipper {
103         private static final Logger LOGGER = LoggerFactory.getLogger(DependencyResolutionSkipper.class);
104 
105         private final Map<DependencyNode, DependencyResolutionResult> results = new LinkedHashMap<>(256);
106         private final CacheManager cacheManager = new CacheManager();
107         private final CoordinateManager coordinateManager = new CoordinateManager();
108 
109         @Override
110         public boolean skipResolution(DependencyNode node, List<DependencyNode> parents) {
111             DependencyResolutionResult result = new DependencyResolutionResult(node);
112             results.put(node, result);
113 
114             int depth = parents.size() + 1;
115             coordinateManager.createCoordinate(node, depth);
116 
117             if (cacheManager.isVersionConflict(node)) {
118                 /*
119                  * Skip resolving version conflict losers (omitted for conflict)
120                  */
121                 result.skippedAsVersionConflict = true;
122                 if (LOGGER.isTraceEnabled()) {
123                     LOGGER.trace(
124                             "Skipped resolving node: {} as version conflict", ArtifactIdUtils.toId(node.getArtifact()));
125                 }
126             } else if (cacheManager.isDuplicate(node)) {
127                 if (coordinateManager.isLeftmost(node, parents)) {
128                     /*
129                      * Force resolving the node to retain conflict paths when its coordinate is
130                      * more left than last resolved
131                      * This is because Maven picks the widest scope present among conflicting dependencies
132                      */
133                     result.forceResolution = true;
134                     if (LOGGER.isTraceEnabled()) {
135                         LOGGER.trace(
136                                 "Force resolving node: {} for scope selection",
137                                 ArtifactIdUtils.toId(node.getArtifact()));
138                     }
139                 } else {
140                     /*
141                      * Skip resolving as duplicate (depth deeper, omitted for duplicate)
142                      * No need to compare depth as the depth of winner for given artifact is always shallower
143                      */
144                     result.skippedAsDuplicate = true;
145                     if (LOGGER.isTraceEnabled()) {
146                         LOGGER.trace(
147                                 "Skipped resolving node: {} as duplicate", ArtifactIdUtils.toId(node.getArtifact()));
148                     }
149                 }
150             } else {
151                 result.resolve = true;
152                 if (LOGGER.isTraceEnabled()) {
153                     LOGGER.trace("Resolving node: {}", ArtifactIdUtils.toId(node.getArtifact()));
154                 }
155             }
156 
157             if (result.toResolve()) {
158                 coordinateManager.updateLeftmost(node);
159                 return false;
160             }
161 
162             return true;
163         }
164 
165         @Override
166         public void cache(DependencyNode node, List<DependencyNode> parents) {
167             boolean parentForceResolution =
168                     parents.stream().anyMatch(n -> results.containsKey(n) && results.get(n).forceResolution);
169             if (parentForceResolution) {
170                 if (LOGGER.isTraceEnabled()) {
171                     LOGGER.trace(
172                             "Won't cache as node: {} inherits from a force-resolved node "
173                                     + "and will be omitted for duplicate",
174                             ArtifactIdUtils.toId(node.getArtifact()));
175                 }
176             } else {
177                 cacheManager.cacheWinner(node);
178             }
179         }
180 
181         @Override
182         public void close() {
183             if (LOGGER.isTraceEnabled()) {
184                 LOGGER.trace(
185                         "Skipped {} nodes as duplicate",
186                         results.entrySet().stream()
187                                 .filter(n -> n.getValue().skippedAsDuplicate)
188                                 .count());
189                 LOGGER.trace(
190                         "Skipped {} nodes as having version conflict",
191                         results.entrySet().stream()
192                                 .filter(n -> n.getValue().skippedAsVersionConflict)
193                                 .count());
194                 LOGGER.trace(
195                         "Resolved {} nodes",
196                         results.entrySet().stream()
197                                 .filter(n -> n.getValue().resolve)
198                                 .count());
199                 LOGGER.trace(
200                         "Forced resolving {} nodes for scope selection",
201                         results.entrySet().stream()
202                                 .filter(n -> n.getValue().forceResolution)
203                                 .count());
204             }
205         }
206 
207         public Map<DependencyNode, DependencyResolutionResult> getResults() {
208             return results;
209         }
210 
211         private static final class CacheManager {
212 
213             /**
214              * artifact -> node
215              */
216             private final Map<Artifact, DependencyNode> winners = new HashMap<>(256);
217 
218             /**
219              * versionLessId -> Artifact, only cache winners
220              */
221             private final Map<String, Artifact> winnerGAs = new HashMap<>(256);
222 
223             boolean isVersionConflict(DependencyNode node) {
224                 String ga = ArtifactIdUtils.toVersionlessId(node.getArtifact());
225                 if (winnerGAs.containsKey(ga)) {
226                     Artifact result = winnerGAs.get(ga);
227                     return !node.getArtifact().getVersion().equals(result.getVersion());
228                 }
229 
230                 return false;
231             }
232 
233             void cacheWinner(DependencyNode node) {
234                 winners.put(node.getArtifact(), node);
235                 winnerGAs.put(ArtifactIdUtils.toVersionlessId(node.getArtifact()), node.getArtifact());
236             }
237 
238             boolean isDuplicate(DependencyNode node) {
239                 return winners.containsKey(node.getArtifact());
240             }
241         }
242 
243         private static final class CoordinateManager {
244             private final Map<Integer, AtomicInteger> sequenceGen = new HashMap<>(256);
245 
246             /**
247              * Dependency node -> Coordinate
248              */
249             private final Map<DependencyNode, Coordinate> coordinateMap = new HashMap<>(256);
250 
251             /**
252              * Leftmost coordinate of given artifact
253              */
254             private final Map<Artifact, Coordinate> leftmostCoordinates = new HashMap<>(256);
255 
256             Coordinate getCoordinate(DependencyNode node) {
257                 return coordinateMap.get(node);
258             }
259 
260             Coordinate createCoordinate(DependencyNode node, int depth) {
261                 int seq = sequenceGen
262                         .computeIfAbsent(depth, k -> new AtomicInteger())
263                         .incrementAndGet();
264                 Coordinate coordinate = new Coordinate(depth, seq);
265                 coordinateMap.put(node, coordinate);
266                 return coordinate;
267             }
268 
269             void updateLeftmost(DependencyNode current) {
270                 leftmostCoordinates.put(current.getArtifact(), getCoordinate(current));
271             }
272 
273             boolean isLeftmost(DependencyNode node, List<DependencyNode> parents) {
274                 Coordinate leftmost = leftmostCoordinates.get(node.getArtifact());
275                 if (leftmost != null && leftmost.depth <= parents.size()) {
276                     DependencyNode sameLevelNode = parents.get(leftmost.depth - 1);
277                     return getCoordinate(sameLevelNode).sequence < leftmost.sequence;
278                 }
279 
280                 return false;
281             }
282         }
283 
284         private static final class Coordinate {
285             int depth;
286             int sequence;
287 
288             Coordinate(int depth, int sequence) {
289                 this.depth = depth;
290                 this.sequence = sequence;
291             }
292 
293             @Override
294             public String toString() {
295                 return "{" + "depth=" + depth + ", sequence=" + sequence + '}';
296             }
297         }
298     }
299 
300     /**
301      * Visible for testing.
302      */
303     static final class DependencyResolutionResult {
304         DependencyNode current;
305         boolean skippedAsVersionConflict; // omitted for conflict
306         boolean skippedAsDuplicate; // omitted for duplicate, depth is deeper
307         boolean resolve; // node to resolve (winner node)
308         boolean forceResolution; // force resolving (duplicate node) for scope selection
309 
310         DependencyResolutionResult(DependencyNode current) {
311             this.current = current;
312         }
313 
314         boolean toResolve() {
315             return resolve || forceResolution;
316         }
317     }
318 }