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.
040 * <p>
041 * This implementation is Maven specific, as it works hand-in-hand along with Maven ModelBuilder. While model builder
042 * handles dependency management in the context of single POM (inheritance, imports, etc.), this implementation carries
043 * in-lineage modifications based on previously recorded dependency management rules sourced from ascendants while
044 * building the dependency graph. Root sourced management rules are special, in a way they are always applied, while
045 * en-route collected ones are carefully applied to proper descendants only, to not override work done by model
046 * builder already.
047 * <p>
048 * Details: Model builder handles version, scope from own dependency management (think "effective POM"). On the other
049 * hand it does not handle optional for example. System paths are aligned across whole graph, making sure there is
050 * same system path used by same dependency. Finally, exclusions (exclusions are additional information not effective
051 * or applied in same POM) are always applied. This implementation makes sure, that version and scope are not applied
052 * onto same node that actually provided the rules, to no override work that ModelBuilder did. It achieves this goal
053 * by tracking "depth" for each collected rule and ignoring rules coming from same depth as processed dependency node is.
054 * <p>
055 * Note for future: the field {@code managedLocalPaths} is <em>intentionally left out of hash/equals</em>, with
056 * reason explained above.
057 * <p>
058 * Implementation note for all managers extending this class: this class maintains "path" (list of parent managers)
059 * and "depth". Depth {@code 0} is basically used as "factory" on session; is the instance created during session
060 * creation and is usually empty (just parameterized). Depth 1 is the current collection "root", depth 2
061 * are direct dependencies, depth 3 first level of transitive dependencies of direct dependencies and so on. Hence, on
062 * depth 1 (the collection root, initialized with management possibly as well) parent will be always the empty "factory"
063 * instance, and we need special handling: "apply onto itself". This does not stand on depth > 1.
064 *
065 * @since 2.0.0
066 */
067public abstract class AbstractDependencyManager implements DependencyManager {
068    protected final ArrayList<AbstractDependencyManager> path;
069
070    protected final int depth;
071
072    protected final int deriveUntil;
073
074    protected final int applyFrom;
075
076    protected final MMap<Key, String> managedVersions;
077
078    protected final MMap<Key, String> managedScopes;
079
080    protected final MMap<Key, Boolean> managedOptionals;
081
082    protected final MMap<Key, String> managedLocalPaths;
083
084    protected final MMap<Key, Holder<Collection<Exclusion>>> managedExclusions;
085
086    protected final SystemDependencyScope systemDependencyScope;
087
088    private final int hashCode;
089
090    protected AbstractDependencyManager(int deriveUntil, int applyFrom, ScopeManager scopeManager) {
091        this(
092                new ArrayList<>(),
093                0,
094                deriveUntil,
095                applyFrom,
096                null,
097                null,
098                null,
099                null,
100                null,
101                scopeManager != null
102                        ? scopeManager.getSystemDependencyScope().orElse(null)
103                        : SystemDependencyScope.LEGACY);
104    }
105
106    @SuppressWarnings("checkstyle:ParameterNumber")
107    protected AbstractDependencyManager(
108            ArrayList<AbstractDependencyManager> path,
109            int depth,
110            int deriveUntil,
111            int applyFrom,
112            MMap<Key, String> managedVersions,
113            MMap<Key, String> managedScopes,
114            MMap<Key, Boolean> managedOptionals,
115            MMap<Key, String> managedLocalPaths,
116            MMap<Key, Holder<Collection<Exclusion>>> managedExclusions,
117            SystemDependencyScope systemDependencyScope) {
118        this.path = path;
119        this.depth = depth;
120        this.deriveUntil = deriveUntil;
121        this.applyFrom = applyFrom;
122        this.managedVersions = managedVersions;
123        this.managedScopes = managedScopes;
124        this.managedOptionals = managedOptionals;
125        this.managedLocalPaths = managedLocalPaths;
126        this.managedExclusions = managedExclusions;
127        // nullable: if using scope manager, but there is no system scope defined
128        this.systemDependencyScope = systemDependencyScope;
129
130        // exclude managedLocalPaths
131        this.hashCode = Objects.hash(path, depth, managedVersions, managedScopes, managedOptionals, managedExclusions);
132    }
133
134    protected abstract DependencyManager newInstance(
135            MMap<Key, String> managedVersions,
136            MMap<Key, String> managedScopes,
137            MMap<Key, Boolean> managedOptionals,
138            MMap<Key, String> managedLocalPaths,
139            MMap<Key, Holder<Collection<Exclusion>>> managedExclusions);
140
141    private boolean containsManagedVersion(Key key) {
142        for (AbstractDependencyManager ancestor : path) {
143            if (ancestor.managedVersions != null && ancestor.managedVersions.containsKey(key)) {
144                return true;
145            }
146        }
147        return managedVersions != null && managedVersions.containsKey(key);
148    }
149
150    private String getManagedVersion(Key key) {
151        for (AbstractDependencyManager ancestor : path) {
152            if (ancestor.managedVersions != null && ancestor.managedVersions.containsKey(key)) {
153                return ancestor.managedVersions.get(key);
154            }
155        }
156        if (depth == 1 && managedVersions != null && managedVersions.containsKey(key)) {
157            return managedVersions.get(key);
158        }
159        return null;
160    }
161
162    private boolean containsManagedScope(Key key) {
163        for (AbstractDependencyManager ancestor : path) {
164            if (ancestor.managedScopes != null && ancestor.managedScopes.containsKey(key)) {
165                return true;
166            }
167        }
168        return managedScopes != null && managedScopes.containsKey(key);
169    }
170
171    private String getManagedScope(Key key) {
172        for (AbstractDependencyManager ancestor : path) {
173            if (ancestor.managedScopes != null && ancestor.managedScopes.containsKey(key)) {
174                return ancestor.managedScopes.get(key);
175            }
176        }
177        if (depth == 1 && managedScopes != null && managedScopes.containsKey(key)) {
178            return managedScopes.get(key);
179        }
180        return null;
181    }
182
183    private boolean containsManagedOptional(Key key) {
184        for (AbstractDependencyManager ancestor : path) {
185            if (ancestor.managedOptionals != null && ancestor.managedOptionals.containsKey(key)) {
186                return true;
187            }
188        }
189        return managedOptionals != null && managedOptionals.containsKey(key);
190    }
191
192    private Boolean getManagedOptional(Key key) {
193        for (AbstractDependencyManager ancestor : path) {
194            if (ancestor.managedOptionals != null && ancestor.managedOptionals.containsKey(key)) {
195                return ancestor.managedOptionals.get(key);
196            }
197        }
198        if (depth == 1 && managedOptionals != null && managedOptionals.containsKey(key)) {
199            return managedOptionals.get(key);
200        }
201        return null;
202    }
203
204    private boolean containsManagedLocalPath(Key key) {
205        for (AbstractDependencyManager ancestor : path) {
206            if (ancestor.managedLocalPaths != null && ancestor.managedLocalPaths.containsKey(key)) {
207                return true;
208            }
209        }
210        return managedLocalPaths != null && managedLocalPaths.containsKey(key);
211    }
212
213    private String getManagedLocalPath(Key key) {
214        for (AbstractDependencyManager ancestor : path) {
215            if (ancestor.managedLocalPaths != null && ancestor.managedLocalPaths.containsKey(key)) {
216                return ancestor.managedLocalPaths.get(key);
217            }
218        }
219        if (managedLocalPaths != null && managedLocalPaths.containsKey(key)) {
220            return managedLocalPaths.get(key);
221        }
222        return null;
223    }
224
225    /**
226     * Merges all way down.
227     */
228    private Collection<Exclusion> getManagedExclusions(Key key) {
229        ArrayList<Exclusion> result = new ArrayList<>();
230        for (AbstractDependencyManager ancestor : path) {
231            if (ancestor.managedExclusions != null && ancestor.managedExclusions.containsKey(key)) {
232                result.addAll(ancestor.managedExclusions.get(key).value);
233            }
234        }
235        if (managedExclusions != null && managedExclusions.containsKey(key)) {
236            result.addAll(managedExclusions.get(key).value);
237        }
238        return result.isEmpty() ? null : result;
239    }
240
241    @Override
242    public DependencyManager deriveChildManager(DependencyCollectionContext context) {
243        requireNonNull(context, "context cannot be null");
244        if (!isDerived()) {
245            return this;
246        }
247
248        MMap<Key, String> managedVersions = null;
249        MMap<Key, String> managedScopes = null;
250        MMap<Key, Boolean> managedOptionals = null;
251        MMap<Key, String> managedLocalPaths = null;
252        MMap<Key, Holder<Collection<Exclusion>>> managedExclusions = null;
253
254        for (Dependency managedDependency : context.getManagedDependencies()) {
255            Artifact artifact = managedDependency.getArtifact();
256            Key key = new Key(artifact);
257
258            String version = artifact.getVersion();
259            if (!version.isEmpty() && !containsManagedVersion(key)) {
260                if (managedVersions == null) {
261                    managedVersions = MMap.emptyNotDone();
262                }
263                managedVersions.put(key, version);
264            }
265
266            String scope = managedDependency.getScope();
267            if (!scope.isEmpty() && !containsManagedScope(key)) {
268                if (managedScopes == null) {
269                    managedScopes = MMap.emptyNotDone();
270                }
271                managedScopes.put(key, scope);
272            }
273
274            Boolean optional = managedDependency.getOptional();
275            if (optional != null && !containsManagedOptional(key)) {
276                if (managedOptionals == null) {
277                    managedOptionals = MMap.emptyNotDone();
278                }
279                managedOptionals.put(key, optional);
280            }
281
282            String localPath = systemDependencyScope == null
283                    ? null
284                    : systemDependencyScope.getSystemPath(managedDependency.getArtifact());
285            if (localPath != null && !containsManagedLocalPath(key)) {
286                if (managedLocalPaths == null) {
287                    managedLocalPaths = MMap.emptyNotDone();
288                }
289                managedLocalPaths.put(key, localPath);
290            }
291
292            Collection<Exclusion> exclusions = managedDependency.getExclusions();
293            if (!exclusions.isEmpty()) {
294                if (managedExclusions == null) {
295                    managedExclusions = MMap.emptyNotDone();
296                }
297                Holder<Collection<Exclusion>> managed = managedExclusions.get(key);
298                if (managed != null) {
299                    ArrayList<Exclusion> ex = new ArrayList<>(managed.getValue());
300                    ex.addAll(exclusions);
301                    managed = new Holder<>(ex);
302                    managedExclusions.put(key, managed);
303                } else {
304                    managedExclusions.put(key, new Holder<>(exclusions));
305                }
306            }
307        }
308
309        return newInstance(
310                managedVersions != null ? managedVersions.done() : null,
311                managedScopes != null ? managedScopes.done() : null,
312                managedOptionals != null ? managedOptionals.done() : null,
313                managedLocalPaths != null ? managedLocalPaths.done() : null,
314                managedExclusions != null ? managedExclusions.done() : null);
315    }
316
317    @Override
318    public DependencyManagement manageDependency(Dependency dependency) {
319        requireNonNull(dependency, "dependency cannot be null");
320        DependencyManagement management = null;
321        Key key = new Key(dependency.getArtifact());
322
323        if (isApplied()) {
324            String version = getManagedVersion(key);
325            // is managed locally by model builder
326            // apply only rules coming from "higher" levels
327            if (version != null) {
328                management = new DependencyManagement();
329                management.setVersion(version);
330            }
331
332            String scope = getManagedScope(key);
333            // is managed locally by model builder
334            // apply only rules coming from "higher" levels
335            if (scope != null) {
336                if (management == null) {
337                    management = new DependencyManagement();
338                }
339                management.setScope(scope);
340
341                if (systemDependencyScope != null
342                        && !systemDependencyScope.is(scope)
343                        && systemDependencyScope.getSystemPath(dependency.getArtifact()) != null) {
344                    HashMap<String, String> properties =
345                            new HashMap<>(dependency.getArtifact().getProperties());
346                    systemDependencyScope.setSystemPath(properties, null);
347                    management.setProperties(properties);
348                }
349            }
350
351            // system scope paths always applied to have them aligned
352            // (same artifact == same path) in whole graph
353            if (systemDependencyScope != null
354                    && (scope != null && systemDependencyScope.is(scope)
355                            || (scope == null && systemDependencyScope.is(dependency.getScope())))) {
356                String localPath = getManagedLocalPath(key);
357                if (localPath != null) {
358                    if (management == null) {
359                        management = new DependencyManagement();
360                    }
361                    HashMap<String, String> properties =
362                            new HashMap<>(dependency.getArtifact().getProperties());
363                    systemDependencyScope.setSystemPath(properties, localPath);
364                    management.setProperties(properties);
365                }
366            }
367
368            // optional is not managed by model builder
369            // apply only rules coming from "higher" levels
370            Boolean optional = getManagedOptional(key);
371            if (optional != null) {
372                if (management == null) {
373                    management = new DependencyManagement();
374                }
375                management.setOptional(optional);
376            }
377        }
378
379        // exclusions affect only downstream
380        // this will not "exclude" own dependency,
381        // is just added as additional information
382        // ModelBuilder does not merge exclusions (only applies if dependency does not have exclusion)
383        // so we merge it here even from same level
384        Collection<Exclusion> exclusions = getManagedExclusions(key);
385        if (exclusions != null) {
386            if (management == null) {
387                management = new DependencyManagement();
388            }
389            Collection<Exclusion> result = new LinkedHashSet<>(dependency.getExclusions());
390            result.addAll(exclusions);
391            management.setExclusions(result);
392        }
393
394        return management;
395    }
396
397    /**
398     * Returns {@code true} if current context should be factored in (collected/derived).
399     */
400    protected boolean isDerived() {
401        return depth < deriveUntil;
402    }
403
404    /**
405     * Returns {@code true} if current dependency should be managed according to so far collected/derived rules.
406     */
407    protected boolean isApplied() {
408        return depth >= applyFrom;
409    }
410
411    @Override
412    public boolean equals(Object obj) {
413        if (this == obj) {
414            return true;
415        } else if (null == obj || !getClass().equals(obj.getClass())) {
416            return false;
417        }
418
419        AbstractDependencyManager that = (AbstractDependencyManager) obj;
420        // exclude managedLocalPaths
421        return Objects.equals(path, that.path)
422                && depth == that.depth
423                && Objects.equals(managedVersions, that.managedVersions)
424                && Objects.equals(managedScopes, that.managedScopes)
425                && Objects.equals(managedOptionals, that.managedOptionals)
426                && Objects.equals(managedExclusions, that.managedExclusions);
427    }
428
429    @Override
430    public int hashCode() {
431        return hashCode;
432    }
433
434    protected static class Key {
435        private final Artifact artifact;
436        private final int hashCode;
437
438        Key(Artifact artifact) {
439            this.artifact = artifact;
440            this.hashCode = Objects.hash(
441                    artifact.getArtifactId(), artifact.getGroupId(), artifact.getExtension(), artifact.getClassifier());
442        }
443
444        @Override
445        public boolean equals(Object obj) {
446            if (obj == this) {
447                return true;
448            } else if (!(obj instanceof Key)) {
449                return false;
450            }
451            Key that = (Key) obj;
452            return artifact.getArtifactId().equals(that.artifact.getArtifactId())
453                    && artifact.getGroupId().equals(that.artifact.getGroupId())
454                    && artifact.getExtension().equals(that.artifact.getExtension())
455                    && artifact.getClassifier().equals(that.artifact.getClassifier());
456        }
457
458        @Override
459        public int hashCode() {
460            return hashCode;
461        }
462
463        @Override
464        public String toString() {
465            return String.valueOf(artifact);
466        }
467    }
468
469    /**
470     * Wrapper class for collection to memoize hash code.
471     *
472     * @param <T> The collection type.
473     */
474    protected static class Holder<T> {
475        private final T value;
476        private final int hashCode;
477
478        Holder(T value) {
479            this.value = requireNonNull(value);
480            this.hashCode = Objects.hash(value);
481        }
482
483        public T getValue() {
484            return value;
485        }
486
487        @Override
488        public boolean equals(Object o) {
489            if (!(o instanceof Holder)) {
490                return false;
491            }
492            Holder<?> holder = (Holder<?>) o;
493            return Objects.equals(value, holder.value);
494        }
495
496        @Override
497        public int hashCode() {
498            return hashCode;
499        }
500    }
501}