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