View Javadoc
1   /*
2    * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and contributors.
3    *
4    * All rights reserved. This program and the accompanying materials
5    * are made available under the terms of the GNU Lesser General Public License
6    * (LGPL) version 2.1 which accompanies this distribution, and is available at
7    * http://www.gnu.org/licenses/lgpl-2.1.html
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12   * Lesser General Public License for more details.
13   *
14   * Contributors:
15   *     bstefanescu, jcarsique, slacoin
16   */
17  package org.nuxeo.build.maven.graph;
18  
19  import java.io.File;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.HashMap;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.SortedMap;
27  import java.util.TreeMap;
28  
29  import org.apache.maven.RepositoryUtils;
30  import org.apache.maven.artifact.Artifact;
31  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
32  import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
33  import org.apache.maven.model.DependencyManagement;
34  import org.apache.maven.model.Exclusion;
35  import org.apache.maven.project.MavenProject;
36  import org.apache.tools.ant.BuildException;
37  import org.apache.tools.ant.Project;
38  import org.codehaus.plexus.util.StringUtils;
39  import org.eclipse.aether.artifact.ArtifactType;
40  import org.eclipse.aether.artifact.ArtifactTypeRegistry;
41  import org.eclipse.aether.collection.CollectRequest;
42  import org.eclipse.aether.collection.CollectResult;
43  import org.eclipse.aether.collection.DependencyCollectionException;
44  import org.eclipse.aether.graph.Dependency;
45  import org.eclipse.aether.graph.DependencyNode;
46  import org.eclipse.aether.resolution.DependencyResult;
47  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
48  import org.eclipse.aether.util.artifact.JavaScopes;
49  import org.nuxeo.build.ant.AntClient;
50  import org.nuxeo.build.maven.AntBuildMojo;
51  import org.nuxeo.build.maven.ArtifactDescriptor;
52  import org.nuxeo.build.maven.filter.Filter;
53  
54  /**
55   * TODO NXBT-258
56   */
57  public class Graph {
58  
59      public final TreeMap<String, Node> nodes = new TreeMap<>();
60  
61      public final List<Node> roots = new LinkedList<>();
62  
63      private AntBuildMojo mojo = AntBuildMojo.getInstance();
64  
65      public List<Node> getRoots() {
66          return roots;
67      }
68  
69      public Collection<Node> getNodes() {
70          return nodes.values();
71      }
72  
73      /**
74       * That methods looks for the pattern and returns the first matching node. It is now deprecated since there are no
75       * use case for it. Use instead {@link #findFirst(String, boolean)} which will fail if two artifacts match the
76       * pattern.
77       *
78       * @deprecated Since 2.0.4. This method is not very interesting.
79       */
80      @Deprecated
81      public Node findFirst(String pattern) {
82          return findFirst(pattern, false);
83      }
84  
85      public Node findFirst(String pattern, boolean stopIfNotUnique) {
86          if (pattern.contains("::")) {
87              // Pattern requiring completion: #findNode(ArtifactDescriptor) must be used instead
88              return null;
89          }
90          SortedMap<String, Node> map = nodes.subMap(pattern + ':', pattern + ((char) (':' + 1)));
91          int size = map.size();
92          if (size == 0) {
93              return null;
94          }
95          if (stopIfNotUnique && size > 1) {
96              AntClient.getInstance().log(
97                      String.format("Pattern '%s' cannot be resolved to a unique node. Matching nodes are: %s", pattern,
98                              map.values()), Project.MSG_DEBUG);
99              return null;
100         }
101         return map.get(map.firstKey());
102     }
103 
104     public Collection<Node> find(String pattern) {
105         SortedMap<String, Node> map = nodes.subMap(pattern + ':', pattern + ((char) (':' + 1)));
106         return map.values();
107     }
108 
109     /**
110      * Add a root node given a Maven Project POM. This can be used to initialize the graph.
111      */
112     public Node addRootNode(MavenProject pom) {
113         Node node = nodes.get(Node.genNodeId(pom.getArtifact()));
114         if (node == null) {
115             node = collectRootNode(pom);
116         }
117         return addRootNode(node);
118     }
119 
120     public Node addRootNode(String key) {
121         ArtifactDescriptor ad = new ArtifactDescriptor(key);
122         return addRootNode(ad.getDependency());
123     }
124 
125     /**
126      * @since 2.0
127      */
128     public Node addRootNode(Dependency dependency) {
129         Node node = nodes.get(Node.genNodeId(dependency));
130         if (node == null) {
131             node = collectRootNode(dependency);
132         } else {
133             roots.add(node);
134             AntClient.getInstance().log("Added root node: " + node, Project.MSG_DEBUG);
135         }
136         return node;
137     }
138 
139     /**
140      * @since 2.0
141      */
142     public Node addRootNode(Node node) {
143         if (!nodes.containsKey(node.id)) {
144             node = collectRootNode(node.getDependency());
145         } else if (!roots.contains(node)) {
146             roots.add(node);
147             AntClient.getInstance().log("Added root node: " + node, Project.MSG_DEBUG);
148         }
149         return nodes.get(node.id);
150     }
151 
152     public Node collectRootNode(Dependency dependency) {
153         DependencyNode root = collectDependencies(dependency);
154         return addRootNode(root);
155     }
156 
157     /**
158      * @since 2.0.4
159      */
160     public Node addRootNode(DependencyNode root) {
161         Node node = new Node(this, root);
162         roots.add(node);
163         AntClient.getInstance().log("Added root node: " + node, Project.MSG_DEBUG);
164         addNode(node);
165         return node;
166     }
167 
168     /**
169      * @since 2.0.4
170      */
171     public Node collectRootNode(MavenProject pom) {
172         DependencyNode root = collectDependencies(pom);
173         return addRootNode(root);
174     }
175 
176     private Node resolveRootNode(Node root, Filter filter, int depth) {
177         if (!filter.accept(root, null)) {
178             return null;
179         }
180         DependencyResult result = DependencyUtils.resolveDependencies(root, filter, depth);
181         Node node = new Node(this, result.getRoot());
182         roots.add(node);
183         AntClient.getInstance().log("Added resolved root node: " + node, Project.MSG_DEBUG);
184         addNode(node);
185         return node;
186     }
187 
188     /**
189      * @since 2.0
190      */
191     public void addNode(Node node) {
192         if (nodes.containsKey(node.getId())) {
193             return;
194         }
195         nodes.put(node.getId(), node);
196         AntClient.getInstance().log("Added node: " + node, Project.MSG_DEBUG);
197         if (!roots.contains(node)) {
198             // Check resolved children follow Maven rules on transitive dependencies scope
199             // https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Transitive_Dependencies
200             List<DependencyNode> removes = new ArrayList<>();
201             for (DependencyNode child : node.getChildren()) {
202                 String childScope = child.getDependency().getScope();
203                 if (JavaScopes.PROVIDED.equals(childScope) || JavaScopes.TEST.equals(childScope)) {
204                     AntClient.getInstance().log("Unexpected child node: " + child + " for " + node, Project.MSG_DEBUG);
205                     removes.add(child);
206                 }
207             }
208             node.getChildren().removeAll(removes);
209         }
210         // Add children
211         for (DependencyNode child : node.getChildren()) {
212             Node childNode = nodes.get(Node.genNodeId(child));
213             if (childNode == null) {
214                 childNode = new Node(this, child);
215                 addNode(childNode);
216             }
217             childNode.addParent(node);
218         }
219     }
220 
221     public Node findNode(ArtifactDescriptor ad) {
222         if (ad.isEmpty()) {
223             return null;
224         }
225         String key = ad.getNodeKeyPattern();
226         Collection<Node> nodesToParse = null;
227         if (key == null) {
228             nodesToParse = getNodes();
229         } else {
230             nodesToParse = find(key);
231         }
232         Node returnNode = null;
233         for (Node node : nodesToParse) {
234             Artifact artifact = node.getMavenArtifact();
235             if (ad.getArtifactId() != null && !ad.getArtifactId().equals(artifact.getArtifactId())) {
236                 continue;
237             }
238             if (ad.getGroupId() != null && !ad.getGroupId().equals(artifact.getGroupId())) {
239                 continue;
240             }
241             if (ad.getVersion() != null && !ad.getVersion().equals(artifact.getVersion())) {
242                 continue;
243             }
244             if (ad.getType() != null && !ad.getType().equals(artifact.getType())) {
245                 continue;
246             }
247             if (ad.getClassifier() != null && !ad.getClassifier().equals(artifact.getClassifier())
248                     || ad.getClassifier() == null && artifact.hasClassifier()) {
249                 continue;
250             }
251             try {
252                 if (returnNode != null
253                         && artifact.getSelectedVersion().compareTo(returnNode.getMavenArtifact().getSelectedVersion()) < 0) {
254                     continue;
255                 }
256             } catch (OverConstrainedVersionException e) {
257                 mojo.getLog().error("Versions comparison failed on " + artifact, e);
258             }
259             returnNode = node;
260         }
261         return returnNode;
262     }
263 
264     public DependencyNode collectDependencies(Dependency dependency) {
265         AntClient.getInstance().log(String.format("Collecting " + dependency), Project.MSG_DEBUG);
266         CollectRequest collectRequest = new CollectRequest(dependency, mojo.getRemoteRepositories());
267         collectRequest.setRequestContext("AAMP graph");
268         return collect(collectRequest);
269     }
270 
271     protected DependencyNode collect(CollectRequest collectRequest) {
272         try {
273             CollectResult result = mojo.getSystem().collectDependencies(mojo.getSession(), collectRequest);
274             DependencyNode node = result.getRoot();
275             AntClient.getInstance().log("Collect result: " + result, Project.MSG_DEBUG);
276             AntClient.getInstance().log("Collect exceptions: " + result.getExceptions(), Project.MSG_DEBUG);
277             AntClient.getInstance()
278                      .log("Direct dependencies: " + String.valueOf(node.getChildren()), Project.MSG_DEBUG);
279             return node;
280         } catch (DependencyCollectionException e) {
281             throw new BuildException("Cannot collect dependency tree for " + collectRequest, e);
282         }
283     }
284 
285     /**
286      * @since 2.0.4
287      */
288     public DependencyNode collectDependencies(MavenProject project) {
289         AntClient.getInstance().log(String.format("Collecting " + project), Project.MSG_DEBUG);
290         CollectRequest collectRequest = new CollectRequest();
291         Artifact rootArtifact = project.getArtifact();
292         rootArtifact.setFile(project.getFile());
293         collectRequest.setRootArtifact(RepositoryUtils.toArtifact(rootArtifact));
294         collectRequest.setRoot(RepositoryUtils.toDependency(rootArtifact, null));
295         collectRequest.setRequestContext("AAMP graph");
296         collectRequest.setRepositories(project.getRemoteProjectRepositories());
297 
298         ArtifactTypeRegistry stereotypes = mojo.getSession().getArtifactTypeRegistry();
299         // BEGIN Code copy from
300         // org.apache.maven.project.DefaultProjectDependenciesResolver.resolve(DependencyResolutionRequest)
301         if (project.getDependencyArtifacts() == null) {
302             for (org.apache.maven.model.Dependency dependency : project.getDependencies()) {
303                 if (StringUtils.isEmpty(dependency.getGroupId()) || StringUtils.isEmpty(dependency.getArtifactId())
304                         || StringUtils.isEmpty(dependency.getVersion())) {
305                     // guard against case where best-effort resolution for invalid models is requested
306                     continue;
307                 }
308                 collectRequest.addDependency(RepositoryUtils.toDependency(dependency, stereotypes));
309             }
310         } else {
311             Map<String, org.apache.maven.model.Dependency> dependencies = new HashMap<>();
312             for (org.apache.maven.model.Dependency dependency : project.getDependencies()) {
313                 String classifier = dependency.getClassifier();
314                 if (classifier == null) {
315                     ArtifactType type = stereotypes.get(dependency.getType());
316                     if (type != null) {
317                         classifier = type.getClassifier();
318                     }
319                 }
320                 String key = ArtifactIdUtils.toVersionlessId(dependency.getGroupId(), dependency.getArtifactId(),
321                         dependency.getType(), classifier);
322                 dependencies.put(key, dependency);
323             }
324             for (Artifact artifact : project.getDependencyArtifacts()) {
325                 String key = artifact.getDependencyConflictId();
326                 org.apache.maven.model.Dependency dependency = dependencies.get(key);
327                 Collection<Exclusion> exclusions = dependency != null ? dependency.getExclusions() : null;
328                 org.eclipse.aether.graph.Dependency dep = RepositoryUtils.toDependency(artifact, exclusions);
329                 if (!JavaScopes.SYSTEM.equals(dep.getScope()) && dep.getArtifact().getFile() != null) {
330                     // enable re-resolution
331                     org.eclipse.aether.artifact.Artifact art = dep.getArtifact();
332                     art = art.setFile(null).setVersion(art.getBaseVersion());
333                     dep = dep.setArtifact(art);
334                 }
335                 collectRequest.addDependency(dep);
336             }
337         }
338 
339         DependencyManagement depMngt = project.getDependencyManagement();
340         if (depMngt != null) {
341             for (org.apache.maven.model.Dependency dependency : depMngt.getDependencies()) {
342                 collectRequest.addManagedDependency(RepositoryUtils.toDependency(dependency, stereotypes));
343             }
344         }
345         // END
346         return collect(collectRequest);
347     }
348 
349     /**
350      * Resolve graph starting from its root nodes.
351      *
352      * @since 2.0
353      */
354     public void resolveDependencies(Filter filter, int depth) {
355         List<Node> oldRoots = new LinkedList<>(roots);
356         roots.clear();
357         nodes.clear();
358         for (Node root : oldRoots) {
359             resolveRootNode(root, filter, depth);
360         }
361     }
362 
363     /**
364      * Try to locally resolve an artifact with its "unique" version.
365      *
366      * @since 1.11.1
367      * @param artifact Artifact to resolve with its unique version
368      * @param e ArtifactNotFoundException originally thrown
369      * @throws ArtifactNotFoundException If alternate resolution failed.
370      * @see Artifact#getBaseVersion()
371      */
372     protected void tryResolutionOnLocalBaseVersion(Artifact artifact, ArtifactNotFoundException e)
373             throws ArtifactNotFoundException {
374         String resolvedVersion = artifact.getVersion();
375         artifact.updateVersion(artifact.getBaseVersion(), mojo.getLocalRepository());
376         File localFile = new File(mojo.getLocalRepository().getBasedir(), mojo.getLocalRepository().pathOf(artifact));
377         if (localFile.exists()) {
378             mojo.getLog().warn(
379                     String.format("Couldn't resolve %s, fallback on local install of unique version %s.",
380                             resolvedVersion, artifact.getBaseVersion()));
381             artifact.setResolved(true);
382         } else {
383             // No success, set back the previous version and raise an error
384             artifact.updateVersion(resolvedVersion, mojo.getLocalRepository());
385             mojo.getLog().warn("Cannot resolve " + artifact, e);
386             throw e;
387         }
388     }
389 
390     /**
391      * @since 2.0
392      */
393     public Node getNode(Dependency dependency) {
394         return nodes.get(Node.genNodeId(dependency));
395     }
396 
397     /**
398      * @param key
399      * @throws BuildException
400      * @since 2.0.4
401      */
402     public Node findNode(String key) throws BuildException {
403         return findNode(key, ArtifactDescriptor.parseQuietly(key));
404     }
405 
406     /**
407      * @param key
408      * @param ad
409      * @throws BuildException
410      * @since 2.0.4
411      */
412     public Node findNode(String key, ArtifactDescriptor ad) throws BuildException {
413         Node node = null;
414         if (key != null) {
415             node = findFirst(key, true);
416         }
417         if (node == null && ad != null) {
418             node = findNode(ad);
419         }
420         if (node == null) {
421             throw new BuildException("Artifact with pattern " + (key != null ? key : ad) + " was not found in graph");
422         }
423         return node;
424     }
425 
426 }