View Javadoc
1   package org.eclipse.aether.internal.impl.collect.bf;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.eclipse.aether.artifact.Artifact;
23  import org.eclipse.aether.graph.DependencyNode;
24  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
25  import org.slf4j.Logger;
26  import org.slf4j.LoggerFactory;
27  
28  import java.util.HashMap;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.concurrent.atomic.AtomicInteger;
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
41  {
42      /**
43       * Check whether the resolution of current node can be skipped before resolving.
44       *
45       * @param node    Current node
46       * @param parents All parent nodes of current node
47       *
48       * @return {@code true} if the node can be skipped for resolution, {@code false} if resolution required.
49       */
50      abstract boolean skipResolution( DependencyNode node, List<DependencyNode> parents );
51  
52      /**
53       * Cache the resolution result when a node is resolved by {@link BfDependencyCollector) after resolution.
54       *
55       * @param node    Current node
56       * @param parents All parent nodes of current node
57       */
58      abstract void cache( DependencyNode node, List<DependencyNode> parents );
59  
60      /**
61       * Print the skip/resolve status report for all nodes.
62       */
63      abstract void report();
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      {
72          return new DefaultDependencyResolutionSkipper();
73      }
74  
75      /**
76       * Returns instance of "never" skipper.
77       */
78      public static DependencyResolutionSkipper neverSkipper()
79      {
80          return NeverDependencyResolutionSkipper.INSTANCE;
81      }
82  
83      /**
84       * NEVER implementation.
85       */
86      private static final class NeverDependencyResolutionSkipper extends DependencyResolutionSkipper
87      {
88          private static final DependencyResolutionSkipper INSTANCE = new NeverDependencyResolutionSkipper();
89  
90          @Override
91          public boolean skipResolution( DependencyNode node, List<DependencyNode> parents )
92          {
93              return false;
94          }
95  
96          @Override
97          public void cache( DependencyNode node, List<DependencyNode> parents )
98          {
99          }
100 
101         @Override
102         public void report()
103         {
104         }
105     }
106 
107     /**
108      * Visible for testing.
109      */
110     static final class DefaultDependencyResolutionSkipper extends DependencyResolutionSkipper
111     {
112         private static final Logger LOGGER = LoggerFactory.getLogger( DependencyResolutionSkipper.class );
113 
114         private final Map<DependencyNode, DependencyResolutionResult> results = new LinkedHashMap<>( 256 );
115         private final CacheManager cacheManager = new CacheManager();
116         private final CoordinateManager coordinateManager = new CoordinateManager();
117 
118         @Override
119         public boolean skipResolution( DependencyNode node, List<DependencyNode> parents )
120         {
121             DependencyResolutionResult result = new DependencyResolutionResult( node );
122             results.put( node, result );
123 
124             int depth = parents.size() + 1;
125             coordinateManager.createCoordinate( node, depth );
126 
127             if ( cacheManager.isVersionConflict( node ) )
128             {
129                 /*
130                  * Skip resolving version conflict losers (omitted for conflict)
131                  */
132                 result.skippedAsVersionConflict = true;
133                 if ( LOGGER.isTraceEnabled() )
134                 {
135                     LOGGER.trace( "Skipped resolving node: {} as version conflict",
136                             ArtifactIdUtils.toId( node.getArtifact() ) );
137                 }
138             }
139             else if ( cacheManager.isDuplicate( node ) )
140             {
141                 if ( coordinateManager.isLeftmost( node, parents ) )
142                 {
143                     /*
144                      * Force resolving the node to retain conflict paths when its coordinate is
145                      * more left than last resolved
146                      * This is because Maven picks the widest scope present among conflicting dependencies
147                      */
148                     result.forceResolution = true;
149                     if ( LOGGER.isTraceEnabled() )
150                     {
151                         LOGGER.trace( "Force resolving node: {} for scope selection",
152                                 ArtifactIdUtils.toId( node.getArtifact() ) );
153                     }
154                 }
155                 else
156                 {
157                     /*
158                      * Skip resolving as duplicate (depth deeper, omitted for duplicate)
159                      * No need to compare depth as the depth of winner for given artifact is always shallower
160                      */
161                     result.skippedAsDuplicate = true;
162                     if ( LOGGER.isTraceEnabled() )
163                     {
164                         LOGGER.trace( "Skipped resolving node: {} as duplicate",
165                                 ArtifactIdUtils.toId( node.getArtifact() ) );
166                     }
167                 }
168             }
169             else
170             {
171                 result.resolve = true;
172                 if ( LOGGER.isTraceEnabled() )
173                 {
174                     LOGGER.trace( "Resolving node: {}",
175                             ArtifactIdUtils.toId( node.getArtifact() ) );
176                 }
177             }
178 
179             if ( result.toResolve() )
180             {
181                 coordinateManager.updateLeftmost( node );
182                 return false;
183             }
184 
185             return true;
186         }
187 
188         @Override
189         public void cache( DependencyNode node, List<DependencyNode> parents )
190         {
191             boolean parentForceResolution = parents.stream()
192                     .anyMatch( n -> results.containsKey( n ) && results.get( n ).forceResolution );
193             if ( parentForceResolution )
194             {
195                 if ( LOGGER.isTraceEnabled() )
196                 {
197                     LOGGER.trace( "Won't cache as node: {} inherits from a force-resolved node "
198                                     + "and will be omitted for duplicate", ArtifactIdUtils.toId( node.getArtifact() ) );
199                 }
200             }
201             else
202             {
203                 cacheManager.cacheWinner( node );
204             }
205         }
206 
207         @Override
208         public void report()
209         {
210             if ( LOGGER.isTraceEnabled() )
211             {
212                 LOGGER.trace( "Skipped {} nodes as duplicate",
213                         results.entrySet().stream().filter( n -> n.getValue().skippedAsDuplicate ).count() );
214                 LOGGER.trace( "Skipped {} nodes as having version conflict",
215                         results.entrySet().stream().filter( n -> n.getValue().skippedAsVersionConflict ).count() );
216                 LOGGER.trace( "Resolved {} nodes",
217                         results.entrySet().stream().filter( n -> n.getValue().resolve ).count() );
218                 LOGGER.trace( "Forced resolving {} nodes for scope selection",
219                         results.entrySet().stream().filter( n -> n.getValue().forceResolution ).count() );
220             }
221         }
222 
223         public Map<DependencyNode, DependencyResolutionResult> getResults()
224         {
225             return results;
226         }
227 
228         private static final class CacheManager
229         {
230 
231             /**
232              * artifact -> node
233              */
234             private final Map<Artifact, DependencyNode> winners = new HashMap<>( 256 );
235 
236 
237             /**
238              * versionLessId -> Artifact, only cache winners
239              */
240             private final Map<String, Artifact> winnerGAs = new HashMap<>( 256 );
241 
242             boolean isVersionConflict( DependencyNode node )
243             {
244                 String ga = ArtifactIdUtils.toVersionlessId( node.getArtifact() );
245                 if ( winnerGAs.containsKey( ga ) )
246                 {
247                     Artifact result = winnerGAs.get( ga );
248                     return !node.getArtifact().getVersion().equals( result.getVersion() );
249                 }
250 
251                 return false;
252             }
253 
254             void cacheWinner( DependencyNode node )
255             {
256                 winners.put( node.getArtifact(), node );
257                 winnerGAs.put( ArtifactIdUtils.toVersionlessId( node.getArtifact() ), node.getArtifact() );
258             }
259 
260             boolean isDuplicate( DependencyNode node )
261             {
262                 return winners.containsKey( node.getArtifact() );
263             }
264 
265         }
266 
267 
268         private static final class CoordinateManager
269         {
270             private final Map<Integer, AtomicInteger> sequenceGen = new HashMap<>( 256 );
271 
272             /**
273              * Dependency node -> Coordinate
274              */
275             private final Map<DependencyNode, Coordinate> coordinateMap = new HashMap<>( 256 );
276 
277             /**
278              * Leftmost coordinate of given artifact
279              */
280             private final Map<Artifact, Coordinate> leftmostCoordinates = new HashMap<>( 256 );
281 
282 
283             Coordinate getCoordinate( DependencyNode node )
284             {
285                 return coordinateMap.get( node );
286             }
287 
288             Coordinate createCoordinate( DependencyNode node, int depth )
289             {
290                 int seq = sequenceGen.computeIfAbsent( depth, k -> new AtomicInteger() ).incrementAndGet();
291                 Coordinate coordinate = new Coordinate( depth, seq );
292                 coordinateMap.put( node, coordinate );
293                 return coordinate;
294             }
295 
296             void updateLeftmost( DependencyNode current )
297             {
298                 leftmostCoordinates.put( current.getArtifact(), getCoordinate( current ) );
299             }
300 
301             boolean isLeftmost( DependencyNode node, List<DependencyNode> parents )
302             {
303                 Coordinate leftmost = leftmostCoordinates.get( node.getArtifact() );
304                 if ( leftmost != null && leftmost.depth <= parents.size() )
305                 {
306                     DependencyNode sameLevelNode = parents.get( leftmost.depth - 1 );
307                     return getCoordinate( sameLevelNode ).sequence < leftmost.sequence;
308                 }
309 
310                 return false;
311             }
312         }
313 
314         private static final class Coordinate
315         {
316             int depth;
317             int sequence;
318 
319             Coordinate( int depth, int sequence )
320             {
321                 this.depth = depth;
322                 this.sequence = sequence;
323             }
324 
325             @Override
326             public String toString()
327             {
328                 return "{"
329                         + "depth="
330                         + depth
331                         + ", sequence="
332                         + sequence
333                         + '}';
334             }
335         }
336     }
337 
338     /**
339      * Visible for testing.
340      */
341     static final class DependencyResolutionResult
342     {
343         DependencyNode current;
344         boolean skippedAsVersionConflict; //omitted for conflict
345         boolean skippedAsDuplicate; //omitted for duplicate, depth is deeper
346         boolean resolve; //node to resolve (winner node)
347         boolean forceResolution; //force resolving (duplicate node) for scope selection
348 
349         DependencyResolutionResult( DependencyNode current )
350         {
351             this.current = current;
352         }
353 
354         boolean toResolve()
355         {
356             return resolve || forceResolution;
357         }
358     }
359 }