001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.util.graph.manager;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.HashMap;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028
029import org.eclipse.aether.artifact.Artifact;
030import org.eclipse.aether.collection.DependencyCollectionContext;
031import org.eclipse.aether.collection.DependencyManagement;
032import org.eclipse.aether.collection.DependencyManager;
033import org.eclipse.aether.graph.Dependency;
034import org.eclipse.aether.graph.Exclusion;
035import org.eclipse.aether.scope.ScopeManager;
036import org.eclipse.aether.scope.SystemDependencyScope;
037
038import static java.util.Objects.requireNonNull;
039
040// Note on lookup semantics: management rules follow "nearest to root wins" precedence.
041// Each instance maintains a cumulative ancestor LayeredMap — a zero-copy cons-list of
042// map fragments — providing O(layers) lookups instead of O(depth) chain walks.
043// The layered map is built incrementally at construction time: child.ancestors =
044// new layer(parent.ancestors, parent.ownData). When the parent has no per-level data,
045// the reference is shared. Since containsManagedXxx() during derive blocks duplicate
046// entries for most properties, there is at most one value per key across all layers.
047
048/**
049 * A dependency manager support class for Maven-specific dependency graph management.
050 *
051 * <h2>Overview</h2>
052 * <p>
053 * This implementation works in conjunction with Maven ModelBuilder to handle dependency
054 * management across the dependency graph. While ModelBuilder manages dependencies within
055 * a single POM context (inheritance, imports), this class applies lineage-based modifications
056 * based on previously recorded dependency management rules sourced from ancestors while
057 * building the dependency graph. Root-sourced management rules are special, in that they are
058 * always applied, while rules collected during traversal are carefully applied to proper
059 * descendants only, to not override work done by ModelBuilder already.
060 * </p>
061 *
062 * <h2>Managed Properties</h2>
063 * <ul>
064 * <li><strong>Version &amp; Scope:</strong> Handled by ModelBuilder for own dependency management
065 *     (think "effective POM"). This implementation ensures these are not applied to the same
066 *     node that provided the rules, to not override ModelBuilder's work.</li>
067 * <li><strong>Optional:</strong> Not handled by ModelBuilder; managed here.</li>
068 * <li><strong>System Paths:</strong> Aligned across the entire graph, ensuring the same
069 *     system path is used by the same dependency.</li>
070 * <li><strong>Exclusions:</strong> Always applied as additional information (not effective
071 *     or applied in the same POM).</li>
072 * </ul>
073 *
074 * <h2>Depth-Based Rule Application</h2>
075 * <p>
076 * This implementation achieves proper rule application by tracking "depth" for each collected
077 * rule and ignoring rules coming from the same depth as the processed dependency node.
078 * </p>
079 * <ul>
080 * <li><strong>Depth 0:</strong> Factory instance created during session initialization and
081 *     parameterized. Collection begins with "derive" operation using root context.</li>
082 * <li><strong>Depth 1:</strong> Special case for "version", "scope" and "optional" properties.
083 *     At this level, "apply onto itself" ensures root-defined rules are applied to first-level
084 *     siblings (which, if managed by ModelBuilder, will be the same, making this a no-op).</li>
085 * <li><strong>Depth > 1:</strong> "Apply onto itself" is not in effect; only "apply below" is used.</li>
086 * </ul>
087 *
088 * <h2>Rule Precedence</h2>
089 * <p>
090 * Rules are keyed by dependency management entry coordinates (GACE: Group, Artifact, Classifier,
091 * Extension - see {@link Key}) and are recorded only if a rule for the same key did not exist
092 * previously. This implements the "nearer (to root) management wins" rule, while root management
093 * overrides all.
094 * </p>
095 *
096 * <h2>Managed Bits and Graph Transformations</h2>
097 * <p>
098 * When a {@link org.eclipse.aether.graph.DependencyNode} becomes "managed" by any property
099 * 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 */
117public 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}