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.util.graph.manager;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.LinkedHashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  
29  import org.eclipse.aether.artifact.Artifact;
30  import org.eclipse.aether.collection.DependencyCollectionContext;
31  import org.eclipse.aether.collection.DependencyManagement;
32  import org.eclipse.aether.collection.DependencyManager;
33  import org.eclipse.aether.graph.Dependency;
34  import org.eclipse.aether.graph.Exclusion;
35  import org.eclipse.aether.scope.ScopeManager;
36  import org.eclipse.aether.scope.SystemDependencyScope;
37  
38  import static java.util.Objects.requireNonNull;
39  
40  // Note on lookup semantics: management rules follow "nearest to root wins" precedence.
41  // Each instance maintains a cumulative ancestor LayeredMap — a zero-copy cons-list of
42  // map fragments — providing O(layers) lookups instead of O(depth) chain walks.
43  // The layered map is built incrementally at construction time: child.ancestors =
44  // new layer(parent.ancestors, parent.ownData). When the parent has no per-level data,
45  // the reference is shared. Since containsManagedXxx() during derive blocks duplicate
46  // entries for most properties, there is at most one value per key across all layers.
47  
48  /**
49   * A dependency manager support class for Maven-specific dependency graph management.
50   *
51   * <h2>Overview</h2>
52   * <p>
53   * This implementation works in conjunction with Maven ModelBuilder to handle dependency
54   * management across the dependency graph. While ModelBuilder manages dependencies within
55   * a single POM context (inheritance, imports), this class applies lineage-based modifications
56   * based on previously recorded dependency management rules sourced from ancestors while
57   * building the dependency graph. Root-sourced management rules are special, in that they are
58   * always applied, while rules collected during traversal are carefully applied to proper
59   * descendants only, to not override work done by ModelBuilder already.
60   * </p>
61   *
62   * <h2>Managed Properties</h2>
63   * <ul>
64   * <li><strong>Version &amp; Scope:</strong> Handled by ModelBuilder for own dependency management
65   *     (think "effective POM"). This implementation ensures these are not applied to the same
66   *     node that provided the rules, to not override ModelBuilder's work.</li>
67   * <li><strong>Optional:</strong> Not handled by ModelBuilder; managed here.</li>
68   * <li><strong>System Paths:</strong> Aligned across the entire graph, ensuring the same
69   *     system path is used by the same dependency.</li>
70   * <li><strong>Exclusions:</strong> Always applied as additional information (not effective
71   *     or applied in the same POM).</li>
72   * </ul>
73   *
74   * <h2>Depth-Based Rule Application</h2>
75   * <p>
76   * This implementation achieves proper rule application by tracking "depth" for each collected
77   * rule and ignoring rules coming from the same depth as the processed dependency node.
78   * </p>
79   * <ul>
80   * <li><strong>Depth 0:</strong> Factory instance created during session initialization and
81   *     parameterized. Collection begins with "derive" operation using root context.</li>
82   * <li><strong>Depth 1:</strong> Special case for "version", "scope" and "optional" properties.
83   *     At this level, "apply onto itself" ensures root-defined rules are applied to first-level
84   *     siblings (which, if managed by ModelBuilder, will be the same, making this a no-op).</li>
85   * <li><strong>Depth > 1:</strong> "Apply onto itself" is not in effect; only "apply below" is used.</li>
86   * </ul>
87   *
88   * <h2>Rule Precedence</h2>
89   * <p>
90   * Rules are keyed by dependency management entry coordinates (GACE: Group, Artifact, Classifier,
91   * Extension - see {@link Key}) and are recorded only if a rule for the same key did not exist
92   * previously. This implements the "nearer (to root) management wins" rule, while root management
93   * overrides all.
94   * </p>
95   *
96   * <h2>Managed Bits and Graph Transformations</h2>
97   * <p>
98   * When a {@link org.eclipse.aether.graph.DependencyNode} becomes "managed" by any property
99   * provided from this manager, {@link org.eclipse.aether.graph.DependencyNode#getManagedBits()}
100  * will carry this information for the given property. Later graph transformations will abstain
101  * from modifying these properties of marked nodes (assuming the node already has the property
102  * set to what it should have). Sometimes this is unwanted, especially for properties that need
103  * to be inherited in the graph (values derived from parent-child context of the actual node,
104  * like "scope" or "optional").
105  * </p>
106  *
107  * <h2>Implementation Notes</h2>
108  * <ul>
109  * <li>This class maintains a "path" (list of parent managers) and "depth".</li>
110  * <li>The field {@code managedLocalPaths} is <em>intentionally left out of hash/equals</em>.</li>
111  * <li>Each dependency "derives" an instance with its own context to process second-level
112  *     dependencies and so on.</li>
113  * </ul>
114  *
115  * @since 2.0.0
116  */
117 public abstract class AbstractDependencyManager implements DependencyManager {
118     /**
119      * Parent manager in the dependency graph (forms a linked list from leaf toward root).
120      * Replaces the previous {@code ArrayList<AbstractDependencyManager> path} field —
121      * siblings share the same parent reference (O(1) derive instead of O(depth) copy).
122      */
123     protected final AbstractDependencyManager parent;
124 
125     /** The current depth in the dependency graph (0 = factory, 1 = root, 2+ = descendants). */
126     protected final int depth;
127 
128     /** Maximum depth for rule derivation (exclusive). */
129     protected final int deriveUntil;
130 
131     /** Minimum depth for rule application (inclusive). */
132     protected final int applyFrom;
133 
134     /** Managed version rules keyed by dependency coordinates. */
135     protected final MMap<Key, String> managedVersions;
136 
137     /** Managed scope rules keyed by dependency coordinates. */
138     protected final MMap<Key, String> managedScopes;
139 
140     /** Managed optional flags keyed by dependency coordinates. */
141     protected final MMap<Key, Boolean> managedOptionals;
142 
143     /** Managed local paths for system dependencies (intentionally excluded from equals/hashCode). */
144     protected final MMap<Key, String> managedLocalPaths;
145 
146     /** Managed exclusions keyed by dependency coordinates. */
147     protected final MMap<Key, Holder<Collection<Exclusion>>> managedExclusions;
148 
149     /** System dependency scope handler, may be null if no system scope is defined. */
150     protected final SystemDependencyScope systemDependencyScope;
151 
152     // ── Cumulative ancestor maps ──────────────────────────────────────────────
153     // Zero-copy layered view of ALL management entries from root through parent.
154     // Each layer holds a reference to the parent layer (older data) and its own entries.
155     // Lookups traverse the chain from newest to oldest — first match wins (O(layers)).
156     // Adding a new level is O(1): just link on top. No HashMap copying.
157     // When the parent has no per-level data, the child shares the same reference.
158     // These are derived data, excluded from equals/hashCode.
159 
160     /** Union of all ancestor version entries (root through parent). */
161     private final LayeredMap<Key, String> ancestorVersions;
162 
163     /** Union of all ancestor scope entries (root through parent). */
164     private final LayeredMap<Key, String> ancestorScopes;
165 
166     /** Union of all ancestor optional entries (root through parent). */
167     private final LayeredMap<Key, Boolean> ancestorOptionals;
168 
169     /** Union of all ancestor local-path entries (root through parent). */
170     private final LayeredMap<Key, String> ancestorLocalPaths;
171 
172     /** Union of all ancestor exclusion entries (root through parent), layered additively. */
173     private final LayeredMap<Key, Collection<Exclusion>> ancestorExclusions;
174 
175     /**
176      * Pre-computed hash code (excludes managedLocalPaths).
177      * Cascading: incorporates the parent's hashCode so a single int comparison
178      * reflects the entire ancestor chain without walking it.
179      */
180     private final int hashCode;
181 
182     /**
183      * Multi-entry memoization cache for {@link #deriveChildManager(DependencyCollectionContext)}:
184      * remembers recent managed-dependency lists (by reference identity) and their results.
185      * <p>
186      * In BFS dependency collection, siblings typically share the same interned managed-dependency
187      * list (guaranteed by DataPool's {@code internArtifactDescriptorManagedDependencies}), but
188      * a reactor with several distinct BOM patterns may alternate between many lists. A 16-entry
189      * ring buffer captures these patterns while keeping constant memory — unlike an unbounded
190      * {@code IdentityHashMap} which would retain every derived {@code DependencyManager} and
191      * prevent GC of the dependency subtrees they reference.
192      * <p>
193      * The BFS collector's traversal loop is single-threaded, so no synchronization is needed.
194      */
195     private static final int MEMO_CACHE_SIZE = 16;
196 
197     @SuppressWarnings("unchecked")
198     private transient List<Dependency>[] memoKeys = new List[MEMO_CACHE_SIZE];
199 
200     private transient DependencyManager[] memoValues = new DependencyManager[MEMO_CACHE_SIZE];
201     private transient int memoIndex;
202 
203     /**
204      * Creates a new dependency manager with the specified derivation and application parameters.
205      *
206      * @param deriveUntil the maximum depth for rule derivation (exclusive), must be >= 0
207      * @param applyFrom the minimum depth for rule application (inclusive), must be >= 0
208      * @param scopeManager the scope manager for handling system dependencies, may be null
209      * @throws IllegalArgumentException if deriveUntil or applyFrom are negative
210      */
211     protected AbstractDependencyManager(int deriveUntil, int applyFrom, ScopeManager scopeManager) {
212         this(
213                 null,
214                 0,
215                 deriveUntil,
216                 applyFrom,
217                 null,
218                 null,
219                 null,
220                 null,
221                 null,
222                 scopeManager != null
223                         ? scopeManager.getSystemDependencyScope().orElse(null)
224                         : SystemDependencyScope.LEGACY);
225     }
226 
227     @SuppressWarnings("checkstyle:ParameterNumber")
228     protected AbstractDependencyManager(
229             AbstractDependencyManager parent,
230             int depth,
231             int deriveUntil,
232             int applyFrom,
233             MMap<Key, String> managedVersions,
234             MMap<Key, String> managedScopes,
235             MMap<Key, Boolean> managedOptionals,
236             MMap<Key, String> managedLocalPaths,
237             MMap<Key, Holder<Collection<Exclusion>>> managedExclusions,
238             SystemDependencyScope systemDependencyScope) {
239         this.parent = parent;
240         this.depth = depth;
241         this.deriveUntil = deriveUntil;
242         this.applyFrom = applyFrom;
243         this.managedVersions = managedVersions;
244         this.managedScopes = managedScopes;
245         this.managedOptionals = managedOptionals;
246         this.managedLocalPaths = managedLocalPaths;
247         this.managedExclusions = managedExclusions;
248         // nullable: if using scope manager, but there is no system scope defined
249         this.systemDependencyScope = systemDependencyScope;
250 
251         // Build cumulative ancestor maps: parent's ancestors + parent's own per-level data.
252         // When parent has no per-level data, the child shares the parent's reference (zero copy).
253         if (parent != null) {
254             this.ancestorVersions = mergeAncestors(parent.ancestorVersions, parent.managedVersions);
255             this.ancestorScopes = mergeAncestors(parent.ancestorScopes, parent.managedScopes);
256             this.ancestorOptionals = mergeAncestors(parent.ancestorOptionals, parent.managedOptionals);
257             this.ancestorLocalPaths = mergeAncestors(parent.ancestorLocalPaths, parent.managedLocalPaths);
258             this.ancestorExclusions = mergeAncestorExclusions(parent.ancestorExclusions, parent.managedExclusions);
259         } else {
260             this.ancestorVersions = null;
261             this.ancestorScopes = null;
262             this.ancestorOptionals = null;
263             this.ancestorLocalPaths = null;
264             this.ancestorExclusions = null;
265         }
266 
267         // Cascading hash: incorporates the parent's pre-computed hash so a single int
268         // comparison reflects the entire ancestor chain. Excludes managedLocalPaths.
269         int h = parent != null ? parent.hashCode : 0;
270         h = 31 * h + depth;
271         h = 31 * h + Objects.hashCode(managedVersions);
272         h = 31 * h + Objects.hashCode(managedScopes);
273         h = 31 * h + Objects.hashCode(managedOptionals);
274         h = 31 * h + Objects.hashCode(managedExclusions);
275         this.hashCode = h;
276     }
277 
278     /**
279      * Links the parent's own per-level MMap on top of the parent's cumulative ancestor layers,
280      * producing the child's cumulative ancestor map. O(1) — no HashMap copying.
281      * When the parent has no per-level data, the parent's layered map is returned as-is.
282      */
283     private static <V> LayeredMap<Key, V> mergeAncestors(LayeredMap<Key, V> parentAncestors, MMap<Key, V> parentOwn) {
284         if (parentAncestors == null && parentOwn == null) {
285             return null;
286         }
287         if (parentOwn == null) {
288             return parentAncestors; // share reference — no new data at this level
289         }
290         return new LayeredMap<>(parentAncestors, parentOwn.delegate);
291     }
292 
293     /**
294      * Links the parent's own per-level exclusions on top of the parent's cumulative ancestor layers.
295      * O(1) — no HashMap copying. Unlike other properties, exclusions use additive semantics:
296      * {@link #getManagedExclusions(Key)} walks all layers to collect the union.
297      */
298     private static LayeredMap<Key, Collection<Exclusion>> mergeAncestorExclusions(
299             LayeredMap<Key, Collection<Exclusion>> parentAncestors,
300             MMap<Key, Holder<Collection<Exclusion>>> parentOwn) {
301         if (parentAncestors == null && parentOwn == null) {
302             return null;
303         }
304         if (parentOwn == null) {
305             return parentAncestors; // share reference
306         }
307         // Unwrap Holder values into a plain map for this layer
308         HashMap<Key, Collection<Exclusion>> ownEntries = new HashMap<>();
309         parentOwn.delegate.forEach((key, holder) -> ownEntries.put(key, holder.getValue()));
310         return new LayeredMap<>(parentAncestors, ownEntries);
311     }
312 
313     protected abstract DependencyManager newInstance(
314             MMap<Key, String> managedVersions,
315             MMap<Key, String> managedScopes,
316             MMap<Key, Boolean> managedOptionals,
317             MMap<Key, String> managedLocalPaths,
318             MMap<Key, Holder<Collection<Exclusion>>> managedExclusions);
319 
320     private boolean containsManagedVersion(Key key, MMap<Key, String> managedVersions) {
321         // Check current instance's own managed versions first (restores the pre-d4035d3a
322         // check that was accidentally dropped when the parameter was introduced).
323         if (this.managedVersions != null && this.managedVersions.containsKey(key)) {
324             return true;
325         }
326         // O(1) lookup in cumulative ancestor map (replaces O(depth) parent chain walk)
327         if (ancestorVersions != null && ancestorVersions.containsKey(key)) {
328             return true;
329         }
330         // Check in-progress new map for duplicates within the same derivation step.
331         return managedVersions != null && managedVersions.containsKey(key);
332     }
333 
334     /**
335      * O(1) lookup in the cumulative ancestor map for the managed version.
336      * At depth 1, also checks own data (root self-application): when DefaultDependencyManager
337      * applies management from depth 0, the root-level rules are stored in DM1.managedVersions
338      * and must be visible to DM1.manageDependency().
339      */
340     private String getManagedVersion(Key key) {
341         String result = ancestorVersions != null ? ancestorVersions.get(key) : null;
342         if (depth == 1 && managedVersions != null && managedVersions.containsKey(key)) {
343             result = managedVersions.get(key);
344         }
345         return result;
346     }
347 
348     private boolean containsManagedScope(Key key, MMap<Key, String> managedScopes) {
349         if (this.managedScopes != null && this.managedScopes.containsKey(key)) {
350             return true;
351         }
352         if (ancestorScopes != null && ancestorScopes.containsKey(key)) {
353             return true;
354         }
355         return managedScopes != null && managedScopes.containsKey(key);
356     }
357 
358     private String getManagedScope(Key key) {
359         String result = ancestorScopes != null ? ancestorScopes.get(key) : null;
360         if (depth == 1 && managedScopes != null && managedScopes.containsKey(key)) {
361             result = managedScopes.get(key);
362         }
363         return result;
364     }
365 
366     private boolean containsManagedOptional(Key key, MMap<Key, Boolean> managedOptionals) {
367         if (this.managedOptionals != null && this.managedOptionals.containsKey(key)) {
368             return true;
369         }
370         if (ancestorOptionals != null && ancestorOptionals.containsKey(key)) {
371             return true;
372         }
373         return managedOptionals != null && managedOptionals.containsKey(key);
374     }
375 
376     private Boolean getManagedOptional(Key key) {
377         Boolean result = ancestorOptionals != null ? ancestorOptionals.get(key) : null;
378         if (depth == 1 && managedOptionals != null && managedOptionals.containsKey(key)) {
379             result = managedOptionals.get(key);
380         }
381         return result;
382     }
383 
384     private boolean containsManagedLocalPath(Key key, MMap<Key, String> managedLocalPaths) {
385         if (this.managedLocalPaths != null && this.managedLocalPaths.containsKey(key)) {
386             return true;
387         }
388         if (ancestorLocalPaths != null && ancestorLocalPaths.containsKey(key)) {
389             return true;
390         }
391         return managedLocalPaths != null && managedLocalPaths.containsKey(key);
392     }
393 
394     /**
395      * Gets the managed local path for system dependencies.
396      * Note: Local paths don't follow the depth=1 special rule like versions/scopes —
397      * own data is always checked (system path alignment across the graph).
398      */
399     private String getManagedLocalPath(Key key) {
400         String result = ancestorLocalPaths != null ? ancestorLocalPaths.get(key) : null;
401         if (managedLocalPaths != null && managedLocalPaths.containsKey(key)) {
402             result = managedLocalPaths.get(key);
403         }
404         return result;
405     }
406 
407     /**
408      * Returns merged exclusions from all ancestor layers plus own exclusions.
409      * Unlike other managed properties, exclusions are accumulated additively
410      * from all levels in the dependency path — each layer is walked to collect
411      * the full union.
412      *
413      * @param key the dependency key
414      * @return merged collection of exclusions, or null if none exist
415      */
416     private Collection<Exclusion> getManagedExclusions(Key key) {
417         // Collect exclusions from all ancestor layers (parent/older layers first)
418         Collection<Exclusion> ancestorExcl = collectExclusionsFromLayers(ancestorExclusions, key);
419         Holder<Collection<Exclusion>> ownExcl = managedExclusions != null ? managedExclusions.get(key) : null;
420 
421         if (ancestorExcl == null && ownExcl == null) {
422             return null;
423         }
424         if (ancestorExcl != null && ownExcl == null) {
425             return ancestorExcl;
426         }
427         if (ancestorExcl == null) {
428             return ownExcl.value;
429         }
430         // Both present: merge additively
431         ArrayList<Exclusion> result = new ArrayList<>(ancestorExcl);
432         result.addAll(ownExcl.value);
433         return result;
434     }
435 
436     /**
437      * Walks all layers of the layered exclusions map, collecting exclusions for the given key.
438      * Parent (older) layers are collected first via recursion to maintain order.
439      * Recursion depth is bounded by the number of layers (typically 2–5), not tree depth.
440      */
441     private static Collection<Exclusion> collectExclusionsFromLayers(
442             LayeredMap<Key, Collection<Exclusion>> layers, Key key) {
443         if (layers == null) {
444             return null;
445         }
446         // Recurse to parent first (older data)
447         Collection<Exclusion> result = collectExclusionsFromLayers(layers.parent, key);
448         Collection<Exclusion> layerExcl = layers.ownEntries.get(key);
449         if (layerExcl != null) {
450             if (result == null) {
451                 result = new ArrayList<>(layerExcl);
452             } else {
453                 result.addAll(layerExcl);
454             }
455         }
456         return result;
457     }
458 
459     @Override
460     public DependencyManager deriveChildManager(DependencyCollectionContext context) {
461         requireNonNull(context, "context cannot be null");
462         if (!isDerived()) {
463             return this;
464         }
465 
466         // Memoization: check if we've already derived for this managed dependencies list
467         // (same object reference — guaranteed by DataPool's descriptor/list interning).
468         // A 4-entry ring buffer captures the common BOM patterns in a large reactor.
469         List<Dependency> managedDeps = context.getManagedDependencies();
470         for (int i = 0; i < MEMO_CACHE_SIZE; i++) {
471             if (managedDeps == memoKeys[i] && memoValues[i] != null) {
472                 return memoValues[i];
473             }
474         }
475 
476         MMap<Key, String> managedVersions = null;
477         MMap<Key, String> managedScopes = null;
478         MMap<Key, Boolean> managedOptionals = null;
479         MMap<Key, String> managedLocalPaths = null;
480         MMap<Key, Holder<Collection<Exclusion>>> managedExclusions = null;
481 
482         for (Dependency managedDependency : managedDeps) {
483             Artifact artifact = managedDependency.getArtifact();
484             Key key = new Key(artifact);
485 
486             String version = artifact.getVersion();
487             if (!version.isEmpty() && !containsManagedVersion(key, managedVersions)) {
488                 if (managedVersions == null) {
489                     managedVersions = MMap.emptyNotDone();
490                 }
491                 managedVersions.put(key, version);
492             }
493 
494             if (isInheritedDerived()) {
495                 String scope = managedDependency.getScope();
496                 if (!scope.isEmpty() && !containsManagedScope(key, managedScopes)) {
497                     if (managedScopes == null) {
498                         managedScopes = MMap.emptyNotDone();
499                     }
500                     managedScopes.put(key, scope);
501                 }
502 
503                 Boolean optional = managedDependency.getOptional();
504                 if (optional != null && !containsManagedOptional(key, managedOptionals)) {
505                     if (managedOptionals == null) {
506                         managedOptionals = MMap.emptyNotDone();
507                     }
508                     managedOptionals.put(key, optional);
509                 }
510             }
511 
512             String localPath = systemDependencyScope == null
513                     ? null
514                     : systemDependencyScope.getSystemPath(managedDependency.getArtifact());
515             if (localPath != null && !containsManagedLocalPath(key, managedLocalPaths)) {
516                 if (managedLocalPaths == null) {
517                     managedLocalPaths = MMap.emptyNotDone();
518                 }
519                 managedLocalPaths.put(key, localPath);
520             }
521 
522             Collection<Exclusion> exclusions = managedDependency.getExclusions();
523             if (!exclusions.isEmpty()) {
524                 if (managedExclusions == null) {
525                     managedExclusions = MMap.emptyNotDone();
526                 }
527                 Holder<Collection<Exclusion>> managed = managedExclusions.get(key);
528                 if (managed != null) {
529                     ArrayList<Exclusion> ex = new ArrayList<>(managed.getValue());
530                     ex.addAll(exclusions);
531                     managed = new Holder<>(ex);
532                     managedExclusions.put(key, managed);
533                 } else {
534                     managedExclusions.put(key, new Holder<>(exclusions));
535                 }
536             }
537         }
538 
539         // Optimization: when no new management data was collected at this depth and management
540         // is already being applied (depth >= applyFrom), reuse this instance. This avoids creating
541         // unnecessarily distinct DependencyManager instances that would defeat the BF collector's
542         // pool cache — the pool key includes the manager, so distinct-but-semantically-equal
543         // managers cause pool misses, which in turn lets the skipper prune subtrees that should
544         // have been served from the cache. This is the common case for transitive dependencies
545         // whose POMs do not declare <dependencyManagement>.
546         //
547         // However, we can only reuse `this` when it carries no management data of its own.
548         // If `this` has management data (e.g. managedVersions != null), returning `this` would
549         // hide that data from the child: getManagedVersion() only checks the parent chain (not
550         // `this.managedVersions`), so a reused instance's own rules become invisible. In that
551         // case we must create a new child with null maps, making `this` the parent and putting
552         // the management data on the parent chain where getManagedVersion() can find it.
553         // See https://github.com/apache/maven-resolver/issues/2013
554         DependencyManager result;
555         if (managedVersions == null
556                 && managedScopes == null
557                 && managedOptionals == null
558                 && managedLocalPaths == null
559                 && managedExclusions == null
560                 && isApplied()
561                 && this.managedVersions == null
562                 && this.managedScopes == null
563                 && this.managedOptionals == null
564                 && this.managedLocalPaths == null
565                 && this.managedExclusions == null) {
566             result = this;
567         } else {
568             result = newInstance(
569                     managedVersions != null ? managedVersions.done() : null,
570                     managedScopes != null ? managedScopes.done() : null,
571                     managedOptionals != null ? managedOptionals.done() : null,
572                     managedLocalPaths != null ? managedLocalPaths.done() : null,
573                     managedExclusions != null ? managedExclusions.done() : null);
574         }
575 
576         // Cache the result in the ring buffer for future calls with the same managed deps list
577         memoKeys[memoIndex] = managedDeps;
578         memoValues[memoIndex] = result;
579         memoIndex = (memoIndex + 1) % MEMO_CACHE_SIZE;
580         return result;
581     }
582 
583     @Override
584     public DependencyManagement manageDependency(Dependency dependency) {
585         requireNonNull(dependency, "dependency cannot be null");
586         DependencyManagement management = null;
587         Key key = new Key(dependency.getArtifact());
588 
589         if (isApplied()) {
590             String version = getManagedVersion(key);
591             // is managed locally by model builder
592             // apply only rules coming from "higher" levels
593             if (version != null) {
594                 management = new DependencyManagement();
595                 management.setVersion(version);
596             }
597 
598             String scope = getManagedScope(key);
599             // is managed locally by model builder
600             // apply only rules coming from "higher" levels
601             if (scope != null) {
602                 if (management == null) {
603                     management = new DependencyManagement();
604                 }
605                 management.setScope(scope);
606 
607                 if (systemDependencyScope != null
608                         && !systemDependencyScope.is(scope)
609                         && systemDependencyScope.getSystemPath(dependency.getArtifact()) != null) {
610                     HashMap<String, String> properties =
611                             new HashMap<>(dependency.getArtifact().getProperties());
612                     systemDependencyScope.setSystemPath(properties, null);
613                     management.setProperties(properties);
614                 }
615             }
616 
617             // system scope paths always applied to have them aligned
618             // (same artifact == same path) in whole graph
619             if (systemDependencyScope != null
620                     && (scope != null && systemDependencyScope.is(scope)
621                             || (scope == null && systemDependencyScope.is(dependency.getScope())))) {
622                 String localPath = getManagedLocalPath(key);
623                 if (localPath != null) {
624                     if (management == null) {
625                         management = new DependencyManagement();
626                     }
627                     HashMap<String, String> properties =
628                             new HashMap<>(dependency.getArtifact().getProperties());
629                     systemDependencyScope.setSystemPath(properties, localPath);
630                     management.setProperties(properties);
631                 }
632             }
633 
634             // optional is not managed by model builder
635             // apply only rules coming from "higher" levels
636             Boolean optional = getManagedOptional(key);
637             if (optional != null) {
638                 if (management == null) {
639                     management = new DependencyManagement();
640                 }
641                 management.setOptional(optional);
642             }
643         }
644 
645         // exclusions affect only downstream
646         // this will not "exclude" own dependency,
647         // is just added as additional information
648         // ModelBuilder does not merge exclusions (only applies if dependency does not have exclusion)
649         // so we merge it here even from same level
650         Collection<Exclusion> exclusions = getManagedExclusions(key);
651         if (exclusions != null) {
652             if (management == null) {
653                 management = new DependencyManagement();
654             }
655             Collection<Exclusion> result = new LinkedHashSet<>(dependency.getExclusions());
656             result.addAll(exclusions);
657             management.setExclusions(result);
658         }
659 
660         return management;
661     }
662 
663     /**
664      * Returns {@code true} if current context should be factored in (collected/derived).
665      */
666     protected boolean isDerived() {
667         return depth < deriveUntil;
668     }
669 
670     /**
671      * Manages dependency properties including "version", "scope", "optional", "local path", and "exclusions".
672      * <p>
673      * Property management behavior:
674      * <ul>
675      * <li><strong>Version:</strong> Follows {@link #isDerived()} pattern. Management is applied only at higher
676      *     levels to avoid interference with the model builder.</li>
677      * <li><strong>Scope:</strong> Derived from root only due to inheritance in dependency graphs. Special handling
678      *     for "system" scope to align artifact paths.</li>
679      * <li><strong>Optional:</strong> Derived from root only due to inheritance in dependency graphs.</li>
680      * <li><strong>Local path:</strong> Managed only when scope is or was set to "system" to ensure consistent
681      *     artifact path alignment.</li>
682      * <li><strong>Exclusions:</strong> Accumulated additively from root to current level throughout the entire
683      *     dependency path.</li>
684      * </ul>
685      * <p>
686      * <strong>Inheritance handling:</strong> Since "scope" and "optional" properties inherit through dependency
687      * graphs (beyond model builder scope), they are derived only from the root node. The actual manager
688      * implementation determines specific handling behavior.
689      * <p>
690      * <strong>Default behavior:</strong> Defaults to {@link #isDerived()} to maintain compatibility with
691      * "classic" behavior (equivalent to {@code deriveUntil=2}). For custom transitivity management, override
692      * this method or ensure inherited managed properties are handled during graph transformation.
693      */
694     protected boolean isInheritedDerived() {
695         return isDerived();
696     }
697 
698     /**
699      * Returns {@code true} if current dependency should be managed according to so far collected/derived rules.
700      */
701     protected boolean isApplied() {
702         return depth >= applyFrom;
703     }
704 
705     @Override
706     public boolean equals(Object obj) {
707         if (this == obj) {
708             return true;
709         } else if (null == obj || !getClass().equals(obj.getClass())) {
710             return false;
711         }
712 
713         AbstractDependencyManager that = (AbstractDependencyManager) obj;
714         // Fast rejection: cascading hashCode reflects the entire ancestor chain,
715         // so a single int mismatch rejects without walking any parent pointers.
716         if (hashCode != that.hashCode) {
717             return false;
718         }
719         // exclude managedLocalPaths
720         // Check cheap fields (depth) before expensive ones (maps, parent chain).
721         // Parent comparison is recursive but each level is hash-guarded, and
722         // shared parents (same identity) short-circuit via the this==obj check.
723         return depth == that.depth
724                 && Objects.equals(managedVersions, that.managedVersions)
725                 && Objects.equals(managedScopes, that.managedScopes)
726                 && Objects.equals(managedOptionals, that.managedOptionals)
727                 && Objects.equals(managedExclusions, that.managedExclusions)
728                 && Objects.equals(parent, that.parent);
729     }
730 
731     @Override
732     public int hashCode() {
733         return hashCode;
734     }
735 
736     /**
737      * Key class for dependency management rules based on GACE coordinates.
738      * GACE = Group, Artifact, Classifier, Extension (excludes version for management purposes).
739      */
740     protected static class Key {
741         private final String groupId;
742         private final String artifactId;
743         private final String extension;
744         private final String classifier;
745         private final int hashCode;
746 
747         /**
748          * Creates a new key from the given artifact's GACE coordinates.
749          * Coordinate strings are cached eagerly to avoid repeated virtual dispatch
750          * through delegation wrappers like {@code RelocatedArtifact} during
751          * {@link #equals} comparisons in hash maps.
752          *
753          * @param artifact the artifact to create a key for
754          */
755         Key(Artifact artifact) {
756             this.groupId = artifact.getGroupId();
757             this.artifactId = artifact.getArtifactId();
758             this.extension = artifact.getExtension();
759             this.classifier = artifact.getClassifier();
760             int h = artifactId.hashCode();
761             h = 31 * h + groupId.hashCode();
762             h = 31 * h + extension.hashCode();
763             h = 31 * h + classifier.hashCode();
764             this.hashCode = h;
765         }
766 
767         @Override
768         public boolean equals(Object obj) {
769             if (obj == this) {
770                 return true;
771             } else if (!(obj instanceof Key)) {
772                 return false;
773             }
774             Key that = (Key) obj;
775             return artifactId.equals(that.artifactId)
776                     && groupId.equals(that.groupId)
777                     && extension.equals(that.extension)
778                     && classifier.equals(that.classifier);
779         }
780 
781         @Override
782         public int hashCode() {
783             return hashCode;
784         }
785 
786         @Override
787         public String toString() {
788             return groupId + ":" + artifactId + ":" + extension + (classifier.isEmpty() ? "" : ":" + classifier);
789         }
790     }
791 
792     /**
793      * Wrapper class for collection to memoize hash code.
794      *
795      * @param <T> the collection type
796      */
797     protected static class Holder<T> {
798         private final T value;
799         private final int hashCode;
800 
801         Holder(T value) {
802             this.value = requireNonNull(value);
803             this.hashCode = value.hashCode();
804         }
805 
806         public T getValue() {
807             return value;
808         }
809 
810         @Override
811         public boolean equals(Object o) {
812             if (!(o instanceof Holder)) {
813                 return false;
814             }
815             Holder<?> holder = (Holder<?>) o;
816             return Objects.equals(value, holder.value);
817         }
818 
819         @Override
820         public int hashCode() {
821             return hashCode;
822         }
823     }
824 
825     /**
826      * A zero-copy layered map built as a cons-list of map fragments.
827      * Each layer holds a reference to its parent (older data) and its own entries.
828      * <p>
829      * For "first match wins" properties (versions, scopes, optionals, local paths),
830      * {@link #get(Object)} returns the first value found traversing from newest to oldest layer.
831      * For additive properties (exclusions), callers walk all layers via the {@link #parent}
832      * pointer to collect the union.
833      * <p>
834      * Adding a new level is O(1) — just link on top. Lookups are O(layers) where layers is
835      * the number of depths that contributed management data (typically 2–5 in practice).
836      *
837      * @param <K> key type
838      * @param <V> value type
839      */
840     static class LayeredMap<K, V> {
841         final LayeredMap<K, V> parent;
842         final Map<K, V> ownEntries;
843 
844         LayeredMap(LayeredMap<K, V> parent, Map<K, V> ownEntries) {
845             this.parent = parent;
846             this.ownEntries = ownEntries;
847         }
848 
849         /** Lookup: newest layer first, O(layers). */
850         V get(K key) {
851             V value = ownEntries.get(key);
852             if (value != null) {
853                 return value;
854             }
855             return parent != null ? parent.get(key) : null;
856         }
857 
858         /** Contains check: any layer, O(layers). */
859         boolean containsKey(K key) {
860             return ownEntries.containsKey(key) || (parent != null && parent.containsKey(key));
861         }
862     }
863 }