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