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 & 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 }