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