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.
40   * <p>
41   * This implementation is Maven specific, as it works hand-in-hand along with Maven ModelBuilder. While model builder
42   * handles dependency management in the context of single POM (inheritance, imports, etc.), this implementation carries
43   * in-lineage modifications based on previously recorded dependency management rules sourced from ascendants while
44   * building the dependency graph. Root sourced management rules are special, in a way they are always applied, while
45   * en-route collected ones are carefully applied to proper descendants only, to not override work done by model
46   * builder already.
47   * <p>
48   * Details: Model builder handles version, scope from own dependency management (think "effective POM"). On the other
49   * hand it does not handle optional for example. System paths are aligned across whole graph, making sure there is
50   * same system path used by same dependency. Finally, exclusions (exclusions are additional information not effective
51   * or applied in same POM) are always applied. This implementation makes sure, that version and scope are not applied
52   * onto same node that actually provided the rules, to no override work that ModelBuilder did. It achieves this goal
53   * by tracking "depth" for each collected rule and ignoring rules coming from same depth as processed dependency node is.
54   * <p>
55   * Note for future: the field {@code managedLocalPaths} is <em>intentionally left out of hash/equals</em>, with
56   * reason explained above.
57   * <p>
58   * Implementation note for all managers extending this class: this class maintains "path" (list of parent managers)
59   * and "depth". Depth {@code 0} is basically used as "factory" on session; is the instance created during session
60   * creation and is usually empty (just parameterized). Depth 1 is the current collection "root", depth 2
61   * are direct dependencies, depth 3 first level of transitive dependencies of direct dependencies and so on. Hence, on
62   * depth 1 (the collection root, initialized with management possibly as well) parent will be always the empty "factory"
63   * instance, and we need special handling: "apply onto itself". This does not stand on depth > 1.
64   *
65   * @since 2.0.0
66   */
67  public abstract class AbstractDependencyManager implements DependencyManager {
68      protected final ArrayList<AbstractDependencyManager> path;
69  
70      protected final int depth;
71  
72      protected final int deriveUntil;
73  
74      protected final int applyFrom;
75  
76      protected final MMap<Key, String> managedVersions;
77  
78      protected final MMap<Key, String> managedScopes;
79  
80      protected final MMap<Key, Boolean> managedOptionals;
81  
82      protected final MMap<Key, String> managedLocalPaths;
83  
84      protected final MMap<Key, Holder<Collection<Exclusion>>> managedExclusions;
85  
86      protected final SystemDependencyScope systemDependencyScope;
87  
88      private final int hashCode;
89  
90      protected AbstractDependencyManager(int deriveUntil, int applyFrom, ScopeManager scopeManager) {
91          this(
92                  new ArrayList<>(),
93                  0,
94                  deriveUntil,
95                  applyFrom,
96                  null,
97                  null,
98                  null,
99                  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 }