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 *
058 * @since 2.0.0
059 */
060public abstract class AbstractDependencyManager implements DependencyManager {
061
062    protected final int depth;
063
064    protected final int deriveUntil;
065
066    protected final int applyFrom;
067
068    protected final MMap<Key, Holder<String>> managedVersions;
069
070    protected final MMap<Key, Holder<String>> managedScopes;
071
072    protected final MMap<Key, Holder<Boolean>> managedOptionals;
073
074    protected final MMap<Key, Holder<String>> managedLocalPaths;
075
076    protected final MMap<Key, Collection<Holder<Collection<Exclusion>>>> managedExclusions;
077
078    protected final SystemDependencyScope systemDependencyScope;
079
080    private final int hashCode;
081
082    protected AbstractDependencyManager(int deriveUntil, int applyFrom, ScopeManager scopeManager) {
083        this(
084                0,
085                deriveUntil,
086                applyFrom,
087                MMap.empty(),
088                MMap.empty(),
089                MMap.empty(),
090                MMap.empty(),
091                MMap.empty(),
092                scopeManager != null
093                        ? scopeManager.getSystemDependencyScope().orElse(null)
094                        : SystemDependencyScope.LEGACY);
095    }
096
097    @SuppressWarnings("checkstyle:ParameterNumber")
098    protected AbstractDependencyManager(
099            int depth,
100            int deriveUntil,
101            int applyFrom,
102            MMap<Key, Holder<String>> managedVersions,
103            MMap<Key, Holder<String>> managedScopes,
104            MMap<Key, Holder<Boolean>> managedOptionals,
105            MMap<Key, Holder<String>> managedLocalPaths,
106            MMap<Key, Collection<Holder<Collection<Exclusion>>>> managedExclusions,
107            SystemDependencyScope systemDependencyScope) {
108        this.depth = depth;
109        this.deriveUntil = deriveUntil;
110        this.applyFrom = applyFrom;
111        this.managedVersions = requireNonNull(managedVersions);
112        this.managedScopes = requireNonNull(managedScopes);
113        this.managedOptionals = requireNonNull(managedOptionals);
114        this.managedLocalPaths = requireNonNull(managedLocalPaths);
115        this.managedExclusions = requireNonNull(managedExclusions);
116        // nullable: if using scope manager, but there is no system scope defined
117        this.systemDependencyScope = systemDependencyScope;
118
119        // exclude managedLocalPaths
120        this.hashCode = Objects.hash(depth, managedVersions, managedScopes, managedOptionals, managedExclusions);
121    }
122
123    protected abstract DependencyManager newInstance(
124            MMap<Key, Holder<String>> managedVersions,
125            MMap<Key, Holder<String>> managedScopes,
126            MMap<Key, Holder<Boolean>> managedOptionals,
127            MMap<Key, Holder<String>> managedLocalPaths,
128            MMap<Key, Collection<Holder<Collection<Exclusion>>>> managedExclusions);
129
130    @Override
131    public DependencyManager deriveChildManager(DependencyCollectionContext context) {
132        requireNonNull(context, "context cannot be null");
133        if (!isDerived()) {
134            return this;
135        }
136
137        MMap<Key, Holder<String>> managedVersions = this.managedVersions;
138        MMap<Key, Holder<String>> managedScopes = this.managedScopes;
139        MMap<Key, Holder<Boolean>> managedOptionals = this.managedOptionals;
140        MMap<Key, Holder<String>> managedLocalPaths = this.managedLocalPaths;
141        MMap<Key, Collection<Holder<Collection<Exclusion>>>> managedExclusions = this.managedExclusions;
142
143        for (Dependency managedDependency : context.getManagedDependencies()) {
144            Artifact artifact = managedDependency.getArtifact();
145            Key key = new Key(artifact);
146
147            String version = artifact.getVersion();
148            if (!version.isEmpty() && !managedVersions.containsKey(key)) {
149                if (managedVersions == this.managedVersions) {
150                    managedVersions = MMap.copy(this.managedVersions);
151                }
152                managedVersions.put(key, new Holder<>(depth, version));
153            }
154
155            String scope = managedDependency.getScope();
156            if (!scope.isEmpty() && !managedScopes.containsKey(key)) {
157                if (managedScopes == this.managedScopes) {
158                    managedScopes = MMap.copy(this.managedScopes);
159                }
160                managedScopes.put(key, new Holder<>(depth, scope));
161            }
162
163            Boolean optional = managedDependency.getOptional();
164            if (optional != null && !managedOptionals.containsKey(key)) {
165                if (managedOptionals == this.managedOptionals) {
166                    managedOptionals = MMap.copy(this.managedOptionals);
167                }
168                managedOptionals.put(key, new Holder<>(depth, optional));
169            }
170
171            String localPath = systemDependencyScope == null
172                    ? null
173                    : systemDependencyScope.getSystemPath(managedDependency.getArtifact());
174            if (localPath != null && !managedLocalPaths.containsKey(key)) {
175                if (managedLocalPaths == this.managedLocalPaths) {
176                    managedLocalPaths = MMap.copy(this.managedLocalPaths);
177                }
178                managedLocalPaths.put(key, new Holder<>(depth, localPath));
179            }
180
181            Collection<Exclusion> exclusions = managedDependency.getExclusions();
182            if (!exclusions.isEmpty()) {
183                if (managedExclusions == this.managedExclusions) {
184                    managedExclusions = MMap.copyWithKey(key, this.managedExclusions);
185                }
186                Collection<Holder<Collection<Exclusion>>> managed = managedExclusions.get(key);
187                if (managed == null) {
188                    managed = new ArrayList<>();
189                    managedExclusions.put(key, managed);
190                }
191                managed.add(new Holder<>(depth, exclusions));
192            }
193        }
194
195        return newInstance(
196                managedVersions.done(),
197                managedScopes.done(),
198                managedOptionals.done(),
199                managedLocalPaths.done(),
200                managedExclusions.done());
201    }
202
203    @Override
204    public DependencyManagement manageDependency(Dependency dependency) {
205        requireNonNull(dependency, "dependency cannot be null");
206        DependencyManagement management = null;
207        Key key = new Key(dependency.getArtifact());
208
209        if (isApplied()) {
210            Holder<String> version = managedVersions.get(key);
211            // is managed locally by model builder
212            // apply only rules coming from "higher" levels
213            if (version != null && isApplicable(version)) {
214                management = new DependencyManagement();
215                management.setVersion(version.getValue());
216            }
217
218            Holder<String> scope = managedScopes.get(key);
219            // is managed locally by model builder
220            // apply only rules coming from "higher" levels
221            if (scope != null && isApplicable(scope)) {
222                if (management == null) {
223                    management = new DependencyManagement();
224                }
225                management.setScope(scope.getValue());
226
227                if (systemDependencyScope != null
228                        && !systemDependencyScope.is(scope.getValue())
229                        && systemDependencyScope.getSystemPath(dependency.getArtifact()) != null) {
230                    HashMap<String, String> properties =
231                            new HashMap<>(dependency.getArtifact().getProperties());
232                    systemDependencyScope.setSystemPath(properties, null);
233                    management.setProperties(properties);
234                }
235            }
236
237            // system scope paths always applied to have them aligned
238            // (same artifact == same path) in whole graph
239            if (systemDependencyScope != null
240                    && (scope != null && systemDependencyScope.is(scope.getValue())
241                            || (scope == null && systemDependencyScope.is(dependency.getScope())))) {
242                Holder<String> localPath = managedLocalPaths.get(key);
243                if (localPath != null) {
244                    if (management == null) {
245                        management = new DependencyManagement();
246                    }
247                    HashMap<String, String> properties =
248                            new HashMap<>(dependency.getArtifact().getProperties());
249                    systemDependencyScope.setSystemPath(properties, localPath.getValue());
250                    management.setProperties(properties);
251                }
252            }
253
254            // optional is not managed by model builder
255            // apply only rules coming from "higher" levels
256            Holder<Boolean> optional = managedOptionals.get(key);
257            if (optional != null && isApplicable(optional)) {
258                if (management == null) {
259                    management = new DependencyManagement();
260                }
261                management.setOptional(optional.getValue());
262            }
263        }
264
265        // exclusions affect only downstream
266        // this will not "exclude" own dependency,
267        // is just added as additional information
268        // ModelBuilder does not merge exclusions (only applies if dependency does not have exclusion)
269        // so we merge it here even from same level
270        Collection<Holder<Collection<Exclusion>>> exclusions = managedExclusions.get(key);
271        if (exclusions != null) {
272            if (management == null) {
273                management = new DependencyManagement();
274            }
275            Collection<Exclusion> result = new LinkedHashSet<>(dependency.getExclusions());
276            for (Holder<Collection<Exclusion>> exclusion : exclusions) {
277                result.addAll(exclusion.getValue());
278            }
279            management.setExclusions(result);
280        }
281
282        return management;
283    }
284
285    /**
286     * Returns {@code true} if current context should be factored in (collected/derived).
287     */
288    protected boolean isDerived() {
289        return depth < deriveUntil;
290    }
291
292    /**
293     * Returns {@code true} if current dependency should be managed according to so far collected/derived rules.
294     */
295    protected boolean isApplied() {
296        return depth >= applyFrom;
297    }
298
299    /**
300     * Returns {@code true} if rule in holder is applicable at current depth.
301     */
302    protected boolean isApplicable(Holder<?> holder) {
303        // explanation: derive collects rules (at given depth) and then last
304        // call newInstance does depth++. This means that distance 1 is still "same node".
305        // Hence, rules from depth - 2 or above should be applied.
306        // root is special: is always applied.
307        return holder.getDepth() == 0 || depth > holder.getDepth() + 1;
308    }
309
310    @Override
311    public boolean equals(Object obj) {
312        if (this == obj) {
313            return true;
314        } else if (null == obj || !getClass().equals(obj.getClass())) {
315            return false;
316        }
317
318        AbstractDependencyManager that = (AbstractDependencyManager) obj;
319        // exclude managedLocalPaths
320        return depth == that.depth
321                && managedVersions.equals(that.managedVersions)
322                && managedScopes.equals(that.managedScopes)
323                && managedOptionals.equals(that.managedOptionals)
324                && managedExclusions.equals(that.managedExclusions);
325    }
326
327    @Override
328    public int hashCode() {
329        return hashCode;
330    }
331
332    protected static class Key {
333        private final Artifact artifact;
334        private final int hashCode;
335
336        Key(Artifact artifact) {
337            this.artifact = artifact;
338            this.hashCode = Objects.hash(
339                    artifact.getArtifactId(), artifact.getGroupId(), artifact.getExtension(), artifact.getClassifier());
340        }
341
342        @Override
343        public boolean equals(Object obj) {
344            if (obj == this) {
345                return true;
346            } else if (!(obj instanceof Key)) {
347                return false;
348            }
349            Key that = (Key) obj;
350            return artifact.getArtifactId().equals(that.artifact.getArtifactId())
351                    && artifact.getGroupId().equals(that.artifact.getGroupId())
352                    && artifact.getExtension().equals(that.artifact.getExtension())
353                    && artifact.getClassifier().equals(that.artifact.getClassifier());
354        }
355
356        @Override
357        public int hashCode() {
358            return hashCode;
359        }
360
361        @Override
362        public String toString() {
363            return String.valueOf(artifact);
364        }
365    }
366
367    protected static class Holder<T> {
368        private final int depth;
369        private final T value;
370        private final int hashCode;
371
372        Holder(int depth, T value) {
373            this.depth = depth;
374            this.value = requireNonNull(value);
375            this.hashCode = Objects.hash(depth, value);
376        }
377
378        public int getDepth() {
379            return depth;
380        }
381
382        public T getValue() {
383            return value;
384        }
385
386        @Override
387        public boolean equals(Object o) {
388            if (!(o instanceof Holder)) {
389                return false;
390            }
391            Holder<?> holder = (Holder<?>) o;
392            return depth == holder.depth && Objects.equals(value, holder.value);
393        }
394
395        @Override
396        public int hashCode() {
397            return hashCode;
398        }
399    }
400}