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