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.Objects; 026 027import org.eclipse.aether.artifact.Artifact; 028import org.eclipse.aether.collection.DependencyCollectionContext; 029import org.eclipse.aether.collection.DependencyManagement; 030import org.eclipse.aether.collection.DependencyManager; 031import org.eclipse.aether.graph.Dependency; 032import org.eclipse.aether.graph.Exclusion; 033import org.eclipse.aether.scope.ScopeManager; 034import org.eclipse.aether.scope.SystemDependencyScope; 035 036import static java.util.Objects.requireNonNull; 037 038/** 039 * A dependency manager support class for Maven-specific dependency graph management. 040 * 041 * <h2>Overview</h2> 042 * <p> 043 * This implementation works in conjunction with Maven ModelBuilder to handle dependency 044 * management across the dependency graph. While ModelBuilder manages dependencies within 045 * a single POM context (inheritance, imports), this class applies lineage-based modifications 046 * based on previously recorded dependency management rules sourced from ancestors while 047 * building the dependency graph. Root-sourced management rules are special, in that they are 048 * always applied, while rules collected during traversal are carefully applied to proper 049 * descendants only, to not override work done by ModelBuilder already. 050 * </p> 051 * 052 * <h2>Managed Properties</h2> 053 * <ul> 054 * <li><strong>Version & Scope:</strong> Handled by ModelBuilder for own dependency management 055 * (think "effective POM"). This implementation ensures these are not applied to the same 056 * node that provided the rules, to not override ModelBuilder's work.</li> 057 * <li><strong>Optional:</strong> Not handled by ModelBuilder; managed here.</li> 058 * <li><strong>System Paths:</strong> Aligned across the entire graph, ensuring the same 059 * system path is used by the same dependency.</li> 060 * <li><strong>Exclusions:</strong> Always applied as additional information (not effective 061 * or applied in the same POM).</li> 062 * </ul> 063 * 064 * <h2>Depth-Based Rule Application</h2> 065 * <p> 066 * This implementation achieves proper rule application by tracking "depth" for each collected 067 * rule and ignoring rules coming from the same depth as the processed dependency node. 068 * </p> 069 * <ul> 070 * <li><strong>Depth 0:</strong> Factory instance created during session initialization and 071 * parameterized. Collection begins with "derive" operation using root context.</li> 072 * <li><strong>Depth 1:</strong> Special case for "version", "scope" and "optional" properties. 073 * At this level, "apply onto itself" ensures root-defined rules are applied to first-level 074 * siblings (which, if managed by ModelBuilder, will be the same, making this a no-op).</li> 075 * <li><strong>Depth > 1:</strong> "Apply onto itself" is not in effect; only "apply below" is used.</li> 076 * </ul> 077 * 078 * <h2>Rule Precedence</h2> 079 * <p> 080 * Rules are keyed by dependency management entry coordinates (GACE: Group, Artifact, Classifier, 081 * Extension - see {@link Key}) and are recorded only if a rule for the same key did not exist 082 * previously. This implements the "nearer (to root) management wins" rule, while root management 083 * overrides all. 084 * </p> 085 * 086 * <h2>Managed Bits and Graph Transformations</h2> 087 * <p> 088 * When a {@link org.eclipse.aether.graph.DependencyNode} becomes "managed" by any property 089 * provided from this manager, {@link org.eclipse.aether.graph.DependencyNode#getManagedBits()} 090 * will carry this information for the given property. Later graph transformations will abstain 091 * from modifying these properties of marked nodes (assuming the node already has the property 092 * set to what it should have). Sometimes this is unwanted, especially for properties that need 093 * to be inherited in the graph (values derived from parent-child context of the actual node, 094 * like "scope" or "optional"). 095 * </p> 096 * 097 * <h2>Implementation Notes</h2> 098 * <ul> 099 * <li>This class maintains a "path" (list of parent managers) and "depth".</li> 100 * <li>The field {@code managedLocalPaths} is <em>intentionally left out of hash/equals</em>.</li> 101 * <li>Each dependency "derives" an instance with its own context to process second-level 102 * dependencies and so on.</li> 103 * </ul> 104 * 105 * @since 2.0.0 106 */ 107public abstract class AbstractDependencyManager implements DependencyManager { 108 /** The path of parent managers from root to current level. */ 109 protected final ArrayList<AbstractDependencyManager> path; 110 111 /** The current depth in the dependency graph (0 = factory, 1 = root, 2+ = descendants). */ 112 protected final int depth; 113 114 /** Maximum depth for rule derivation (exclusive). */ 115 protected final int deriveUntil; 116 117 /** Minimum depth for rule application (inclusive). */ 118 protected final int applyFrom; 119 120 /** Managed version rules keyed by dependency coordinates. */ 121 protected final MMap<Key, String> managedVersions; 122 123 /** Managed scope rules keyed by dependency coordinates. */ 124 protected final MMap<Key, String> managedScopes; 125 126 /** Managed optional flags keyed by dependency coordinates. */ 127 protected final MMap<Key, Boolean> managedOptionals; 128 129 /** Managed local paths for system dependencies (intentionally excluded from equals/hashCode). */ 130 protected final MMap<Key, String> managedLocalPaths; 131 132 /** Managed exclusions keyed by dependency coordinates. */ 133 protected final MMap<Key, Holder<Collection<Exclusion>>> managedExclusions; 134 135 /** System dependency scope handler, may be null if no system scope is defined. */ 136 protected final SystemDependencyScope systemDependencyScope; 137 138 /** Pre-computed hash code (excludes managedLocalPaths). */ 139 private final int hashCode; 140 141 /** 142 * Creates a new dependency manager with the specified derivation and application parameters. 143 * 144 * @param deriveUntil the maximum depth for rule derivation (exclusive), must be >= 0 145 * @param applyFrom the minimum depth for rule application (inclusive), must be >= 0 146 * @param scopeManager the scope manager for handling system dependencies, may be null 147 * @throws IllegalArgumentException if deriveUntil or applyFrom are negative 148 */ 149 protected AbstractDependencyManager(int deriveUntil, int applyFrom, ScopeManager scopeManager) { 150 this( 151 new ArrayList<>(), 152 0, 153 deriveUntil, 154 applyFrom, 155 null, 156 null, 157 null, 158 null, 159 null, 160 scopeManager != null 161 ? scopeManager.getSystemDependencyScope().orElse(null) 162 : SystemDependencyScope.LEGACY); 163 } 164 165 @SuppressWarnings("checkstyle:ParameterNumber") 166 protected AbstractDependencyManager( 167 ArrayList<AbstractDependencyManager> path, 168 int depth, 169 int deriveUntil, 170 int applyFrom, 171 MMap<Key, String> managedVersions, 172 MMap<Key, String> managedScopes, 173 MMap<Key, Boolean> managedOptionals, 174 MMap<Key, String> managedLocalPaths, 175 MMap<Key, Holder<Collection<Exclusion>>> managedExclusions, 176 SystemDependencyScope systemDependencyScope) { 177 this.path = path; 178 this.depth = depth; 179 this.deriveUntil = deriveUntil; 180 this.applyFrom = applyFrom; 181 this.managedVersions = managedVersions; 182 this.managedScopes = managedScopes; 183 this.managedOptionals = managedOptionals; 184 this.managedLocalPaths = managedLocalPaths; 185 this.managedExclusions = managedExclusions; 186 // nullable: if using scope manager, but there is no system scope defined 187 this.systemDependencyScope = systemDependencyScope; 188 189 // exclude managedLocalPaths 190 this.hashCode = Objects.hash(path, depth, managedVersions, managedScopes, managedOptionals, managedExclusions); 191 } 192 193 protected abstract DependencyManager newInstance( 194 MMap<Key, String> managedVersions, 195 MMap<Key, String> managedScopes, 196 MMap<Key, Boolean> managedOptionals, 197 MMap<Key, String> managedLocalPaths, 198 MMap<Key, Holder<Collection<Exclusion>>> managedExclusions); 199 200 private boolean containsManagedVersion(Key key) { 201 for (AbstractDependencyManager ancestor : path) { 202 if (ancestor.managedVersions != null && ancestor.managedVersions.containsKey(key)) { 203 return true; 204 } 205 } 206 return managedVersions != null && managedVersions.containsKey(key); 207 } 208 209 private String getManagedVersion(Key key) { 210 for (AbstractDependencyManager ancestor : path) { 211 if (ancestor.managedVersions != null && ancestor.managedVersions.containsKey(key)) { 212 return ancestor.managedVersions.get(key); 213 } 214 } 215 if (depth == 1 && managedVersions != null && managedVersions.containsKey(key)) { 216 return managedVersions.get(key); 217 } 218 return null; 219 } 220 221 private boolean containsManagedScope(Key key) { 222 for (AbstractDependencyManager ancestor : path) { 223 if (ancestor.managedScopes != null && ancestor.managedScopes.containsKey(key)) { 224 return true; 225 } 226 } 227 return managedScopes != null && managedScopes.containsKey(key); 228 } 229 230 private String getManagedScope(Key key) { 231 for (AbstractDependencyManager ancestor : path) { 232 if (ancestor.managedScopes != null && ancestor.managedScopes.containsKey(key)) { 233 return ancestor.managedScopes.get(key); 234 } 235 } 236 if (depth == 1 && managedScopes != null && managedScopes.containsKey(key)) { 237 return managedScopes.get(key); 238 } 239 return null; 240 } 241 242 private boolean containsManagedOptional(Key key) { 243 for (AbstractDependencyManager ancestor : path) { 244 if (ancestor.managedOptionals != null && ancestor.managedOptionals.containsKey(key)) { 245 return true; 246 } 247 } 248 return managedOptionals != null && managedOptionals.containsKey(key); 249 } 250 251 private Boolean getManagedOptional(Key key) { 252 for (AbstractDependencyManager ancestor : path) { 253 if (ancestor.managedOptionals != null && ancestor.managedOptionals.containsKey(key)) { 254 return ancestor.managedOptionals.get(key); 255 } 256 } 257 if (depth == 1 && managedOptionals != null && managedOptionals.containsKey(key)) { 258 return managedOptionals.get(key); 259 } 260 return null; 261 } 262 263 private boolean containsManagedLocalPath(Key key) { 264 for (AbstractDependencyManager ancestor : path) { 265 if (ancestor.managedLocalPaths != null && ancestor.managedLocalPaths.containsKey(key)) { 266 return true; 267 } 268 } 269 return managedLocalPaths != null && managedLocalPaths.containsKey(key); 270 } 271 272 /** 273 * Gets the managed local path for system dependencies. 274 * Note: Local paths don't follow the depth=1 special rule like versions/scopes. 275 * 276 * @param key the dependency key 277 * @return the managed local path, or null if not managed 278 */ 279 private String getManagedLocalPath(Key key) { 280 for (AbstractDependencyManager ancestor : path) { 281 if (ancestor.managedLocalPaths != null && ancestor.managedLocalPaths.containsKey(key)) { 282 return ancestor.managedLocalPaths.get(key); 283 } 284 } 285 if (managedLocalPaths != null && managedLocalPaths.containsKey(key)) { 286 return managedLocalPaths.get(key); 287 } 288 return null; 289 } 290 291 /** 292 * Merges exclusions from all levels in the dependency path. 293 * Unlike other managed properties, exclusions are accumulated additively 294 * from root to current level throughout the entire dependency path. 295 * 296 * @param key the dependency key 297 * @return merged collection of exclusions, or null if none exist 298 */ 299 private Collection<Exclusion> getManagedExclusions(Key key) { 300 ArrayList<Exclusion> result = new ArrayList<>(); 301 for (AbstractDependencyManager ancestor : path) { 302 if (ancestor.managedExclusions != null && ancestor.managedExclusions.containsKey(key)) { 303 result.addAll(ancestor.managedExclusions.get(key).value); 304 } 305 } 306 if (managedExclusions != null && managedExclusions.containsKey(key)) { 307 result.addAll(managedExclusions.get(key).value); 308 } 309 return result.isEmpty() ? null : result; 310 } 311 312 @Override 313 public DependencyManager deriveChildManager(DependencyCollectionContext context) { 314 requireNonNull(context, "context cannot be null"); 315 if (!isDerived()) { 316 return this; 317 } 318 319 MMap<Key, String> managedVersions = null; 320 MMap<Key, String> managedScopes = null; 321 MMap<Key, Boolean> managedOptionals = null; 322 MMap<Key, String> managedLocalPaths = null; 323 MMap<Key, Holder<Collection<Exclusion>>> managedExclusions = null; 324 325 for (Dependency managedDependency : context.getManagedDependencies()) { 326 Artifact artifact = managedDependency.getArtifact(); 327 Key key = new Key(artifact); 328 329 String version = artifact.getVersion(); 330 if (!version.isEmpty() && !containsManagedVersion(key)) { 331 if (managedVersions == null) { 332 managedVersions = MMap.emptyNotDone(); 333 } 334 managedVersions.put(key, version); 335 } 336 337 if (isInheritedDerived()) { 338 String scope = managedDependency.getScope(); 339 if (!scope.isEmpty() && !containsManagedScope(key)) { 340 if (managedScopes == null) { 341 managedScopes = MMap.emptyNotDone(); 342 } 343 managedScopes.put(key, scope); 344 } 345 346 Boolean optional = managedDependency.getOptional(); 347 if (optional != null && !containsManagedOptional(key)) { 348 if (managedOptionals == null) { 349 managedOptionals = MMap.emptyNotDone(); 350 } 351 managedOptionals.put(key, optional); 352 } 353 } 354 355 String localPath = systemDependencyScope == null 356 ? null 357 : systemDependencyScope.getSystemPath(managedDependency.getArtifact()); 358 if (localPath != null && !containsManagedLocalPath(key)) { 359 if (managedLocalPaths == null) { 360 managedLocalPaths = MMap.emptyNotDone(); 361 } 362 managedLocalPaths.put(key, localPath); 363 } 364 365 Collection<Exclusion> exclusions = managedDependency.getExclusions(); 366 if (!exclusions.isEmpty()) { 367 if (managedExclusions == null) { 368 managedExclusions = MMap.emptyNotDone(); 369 } 370 Holder<Collection<Exclusion>> managed = managedExclusions.get(key); 371 if (managed != null) { 372 ArrayList<Exclusion> ex = new ArrayList<>(managed.getValue()); 373 ex.addAll(exclusions); 374 managed = new Holder<>(ex); 375 managedExclusions.put(key, managed); 376 } else { 377 managedExclusions.put(key, new Holder<>(exclusions)); 378 } 379 } 380 } 381 382 return newInstance( 383 managedVersions != null ? managedVersions.done() : null, 384 managedScopes != null ? managedScopes.done() : null, 385 managedOptionals != null ? managedOptionals.done() : null, 386 managedLocalPaths != null ? managedLocalPaths.done() : null, 387 managedExclusions != null ? managedExclusions.done() : null); 388 } 389 390 @Override 391 public DependencyManagement manageDependency(Dependency dependency) { 392 requireNonNull(dependency, "dependency cannot be null"); 393 DependencyManagement management = null; 394 Key key = new Key(dependency.getArtifact()); 395 396 if (isApplied()) { 397 String version = getManagedVersion(key); 398 // is managed locally by model builder 399 // apply only rules coming from "higher" levels 400 if (version != null) { 401 management = new DependencyManagement(); 402 management.setVersion(version); 403 } 404 405 String scope = getManagedScope(key); 406 // is managed locally by model builder 407 // apply only rules coming from "higher" levels 408 if (scope != null) { 409 if (management == null) { 410 management = new DependencyManagement(); 411 } 412 management.setScope(scope); 413 414 if (systemDependencyScope != null 415 && !systemDependencyScope.is(scope) 416 && systemDependencyScope.getSystemPath(dependency.getArtifact()) != null) { 417 HashMap<String, String> properties = 418 new HashMap<>(dependency.getArtifact().getProperties()); 419 systemDependencyScope.setSystemPath(properties, null); 420 management.setProperties(properties); 421 } 422 } 423 424 // system scope paths always applied to have them aligned 425 // (same artifact == same path) in whole graph 426 if (systemDependencyScope != null 427 && (scope != null && systemDependencyScope.is(scope) 428 || (scope == null && systemDependencyScope.is(dependency.getScope())))) { 429 String localPath = getManagedLocalPath(key); 430 if (localPath != null) { 431 if (management == null) { 432 management = new DependencyManagement(); 433 } 434 HashMap<String, String> properties = 435 new HashMap<>(dependency.getArtifact().getProperties()); 436 systemDependencyScope.setSystemPath(properties, localPath); 437 management.setProperties(properties); 438 } 439 } 440 441 // optional is not managed by model builder 442 // apply only rules coming from "higher" levels 443 Boolean optional = getManagedOptional(key); 444 if (optional != null) { 445 if (management == null) { 446 management = new DependencyManagement(); 447 } 448 management.setOptional(optional); 449 } 450 } 451 452 // exclusions affect only downstream 453 // this will not "exclude" own dependency, 454 // is just added as additional information 455 // ModelBuilder does not merge exclusions (only applies if dependency does not have exclusion) 456 // so we merge it here even from same level 457 Collection<Exclusion> exclusions = getManagedExclusions(key); 458 if (exclusions != null) { 459 if (management == null) { 460 management = new DependencyManagement(); 461 } 462 Collection<Exclusion> result = new LinkedHashSet<>(dependency.getExclusions()); 463 result.addAll(exclusions); 464 management.setExclusions(result); 465 } 466 467 return management; 468 } 469 470 /** 471 * Returns {@code true} if current context should be factored in (collected/derived). 472 */ 473 protected boolean isDerived() { 474 return depth < deriveUntil; 475 } 476 477 /** 478 * Manages dependency properties including "version", "scope", "optional", "local path", and "exclusions". 479 * <p> 480 * Property management behavior: 481 * <ul> 482 * <li><strong>Version:</strong> Follows {@link #isDerived()} pattern. Management is applied only at higher 483 * levels to avoid interference with the model builder.</li> 484 * <li><strong>Scope:</strong> Derived from root only due to inheritance in dependency graphs. Special handling 485 * for "system" scope to align artifact paths.</li> 486 * <li><strong>Optional:</strong> Derived from root only due to inheritance in dependency graphs.</li> 487 * <li><strong>Local path:</strong> Managed only when scope is or was set to "system" to ensure consistent 488 * artifact path alignment.</li> 489 * <li><strong>Exclusions:</strong> Accumulated additively from root to current level throughout the entire 490 * dependency path.</li> 491 * </ul> 492 * <p> 493 * <strong>Inheritance handling:</strong> Since "scope" and "optional" properties inherit through dependency 494 * graphs (beyond model builder scope), they are derived only from the root node. The actual manager 495 * implementation determines specific handling behavior. 496 * <p> 497 * <strong>Default behavior:</strong> Defaults to {@link #isDerived()} to maintain compatibility with 498 * "classic" behavior (equivalent to {@code deriveUntil=2}). For custom transitivity management, override 499 * this method or ensure inherited managed properties are handled during graph transformation. 500 */ 501 protected boolean isInheritedDerived() { 502 return isDerived(); 503 } 504 505 /** 506 * Returns {@code true} if current dependency should be managed according to so far collected/derived rules. 507 */ 508 protected boolean isApplied() { 509 return depth >= applyFrom; 510 } 511 512 @Override 513 public boolean equals(Object obj) { 514 if (this == obj) { 515 return true; 516 } else if (null == obj || !getClass().equals(obj.getClass())) { 517 return false; 518 } 519 520 AbstractDependencyManager that = (AbstractDependencyManager) obj; 521 // exclude managedLocalPaths 522 return Objects.equals(path, that.path) 523 && depth == that.depth 524 && Objects.equals(managedVersions, that.managedVersions) 525 && Objects.equals(managedScopes, that.managedScopes) 526 && Objects.equals(managedOptionals, that.managedOptionals) 527 && Objects.equals(managedExclusions, that.managedExclusions); 528 } 529 530 @Override 531 public int hashCode() { 532 return hashCode; 533 } 534 535 /** 536 * Key class for dependency management rules based on GACE coordinates. 537 * GACE = Group, Artifact, Classifier, Extension (excludes version for management purposes). 538 */ 539 protected static class Key { 540 private final Artifact artifact; 541 private final int hashCode; 542 543 /** 544 * Creates a new key from the given artifact's GACE coordinates. 545 * 546 * @param artifact the artifact to create a key for 547 */ 548 Key(Artifact artifact) { 549 this.artifact = artifact; 550 this.hashCode = Objects.hash( 551 artifact.getArtifactId(), artifact.getGroupId(), artifact.getExtension(), artifact.getClassifier()); 552 } 553 554 @Override 555 public boolean equals(Object obj) { 556 if (obj == this) { 557 return true; 558 } else if (!(obj instanceof Key)) { 559 return false; 560 } 561 Key that = (Key) obj; 562 return artifact.getArtifactId().equals(that.artifact.getArtifactId()) 563 && artifact.getGroupId().equals(that.artifact.getGroupId()) 564 && artifact.getExtension().equals(that.artifact.getExtension()) 565 && artifact.getClassifier().equals(that.artifact.getClassifier()); 566 } 567 568 @Override 569 public int hashCode() { 570 return hashCode; 571 } 572 573 @Override 574 public String toString() { 575 return String.valueOf(artifact); 576 } 577 } 578 579 /** 580 * Wrapper class for collection to memoize hash code. 581 * 582 * @param <T> the collection type 583 */ 584 protected static class Holder<T> { 585 private final T value; 586 private final int hashCode; 587 588 Holder(T value) { 589 this.value = requireNonNull(value); 590 this.hashCode = Objects.hash(value); 591 } 592 593 public T getValue() { 594 return value; 595 } 596 597 @Override 598 public boolean equals(Object o) { 599 if (!(o instanceof Holder)) { 600 return false; 601 } 602 Holder<?> holder = (Holder<?>) o; 603 return Objects.equals(value, holder.value); 604 } 605 606 @Override 607 public int hashCode() { 608 return hashCode; 609 } 610 } 611}