View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.util.graph.manager;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.LinkedHashSet;
25  import java.util.Objects;
26  
27  import org.eclipse.aether.artifact.Artifact;
28  import org.eclipse.aether.collection.DependencyCollectionContext;
29  import org.eclipse.aether.collection.DependencyManagement;
30  import org.eclipse.aether.collection.DependencyManager;
31  import org.eclipse.aether.graph.Dependency;
32  import org.eclipse.aether.graph.Exclusion;
33  import org.eclipse.aether.scope.ScopeManager;
34  import org.eclipse.aether.scope.SystemDependencyScope;
35  
36  import static java.util.Objects.requireNonNull;
37  
38  /**
39   * A dependency manager support class for Maven-specific dependency graph management.
40   *
41   * <h2>Overview</h2>
42   * <p>
43   * This implementation works in conjunction with Maven ModelBuilder to handle dependency
44   * management across the dependency graph. While ModelBuilder manages dependencies within
45   * a single POM context (inheritance, imports), this class applies lineage-based modifications
46   * based on previously recorded dependency management rules sourced from ancestors while
47   * building the dependency graph. Root-sourced management rules are special, in that they are
48   * always applied, while rules collected during traversal are carefully applied to proper
49   * descendants only, to not override work done by ModelBuilder already.
50   * </p>
51   *
52   * <h2>Managed Properties</h2>
53   * <ul>
54   * <li><strong>Version &amp; Scope:</strong> Handled by ModelBuilder for own dependency management
55   *     (think "effective POM"). This implementation ensures these are not applied to the same
56   *     node that provided the rules, to not override ModelBuilder's work.</li>
57   * <li><strong>Optional:</strong> Not handled by ModelBuilder; managed here.</li>
58   * <li><strong>System Paths:</strong> Aligned across the entire graph, ensuring the same
59   *     system path is used by the same dependency.</li>
60   * <li><strong>Exclusions:</strong> Always applied as additional information (not effective
61   *     or applied in the same POM).</li>
62   * </ul>
63   *
64   * <h2>Depth-Based Rule Application</h2>
65   * <p>
66   * This implementation achieves proper rule application by tracking "depth" for each collected
67   * rule and ignoring rules coming from the same depth as the processed dependency node.
68   * </p>
69   * <ul>
70   * <li><strong>Depth 0:</strong> Factory instance created during session initialization and
71   *     parameterized. Collection begins with "derive" operation using root context.</li>
72   * <li><strong>Depth 1:</strong> Special case for "version", "scope" and "optional" properties.
73   *     At this level, "apply onto itself" ensures root-defined rules are applied to first-level
74   *     siblings (which, if managed by ModelBuilder, will be the same, making this a no-op).</li>
75   * <li><strong>Depth > 1:</strong> "Apply onto itself" is not in effect; only "apply below" is used.</li>
76   * </ul>
77   *
78   * <h2>Rule Precedence</h2>
79   * <p>
80   * Rules are keyed by dependency management entry coordinates (GACE: Group, Artifact, Classifier,
81   * Extension - see {@link Key}) and are recorded only if a rule for the same key did not exist
82   * previously. This implements the "nearer (to root) management wins" rule, while root management
83   * overrides all.
84   * </p>
85   *
86   * <h2>Managed Bits and Graph Transformations</h2>
87   * <p>
88   * When a {@link org.eclipse.aether.graph.DependencyNode} becomes "managed" by any property
89   * provided from this manager, {@link org.eclipse.aether.graph.DependencyNode#getManagedBits()}
90   * will carry this information for the given property. Later graph transformations will abstain
91   * from modifying these properties of marked nodes (assuming the node already has the property
92   * set to what it should have). Sometimes this is unwanted, especially for properties that need
93   * to be inherited in the graph (values derived from parent-child context of the actual node,
94   * like "scope" or "optional").
95   * </p>
96   *
97   * <h2>Implementation Notes</h2>
98   * <ul>
99   * <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  */
107 public 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 }