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.internal.impl.collect;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.LinkedHashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.concurrent.atomic.AtomicInteger;
29  
30  import org.eclipse.aether.DefaultRepositorySystemSession;
31  import org.eclipse.aether.RepositoryException;
32  import org.eclipse.aether.RepositorySystemSession;
33  import org.eclipse.aether.RequestTrace;
34  import org.eclipse.aether.artifact.Artifact;
35  import org.eclipse.aether.collection.CollectRequest;
36  import org.eclipse.aether.collection.CollectResult;
37  import org.eclipse.aether.collection.DependencyCollectionChecker;
38  import org.eclipse.aether.collection.DependencyCollectionException;
39  import org.eclipse.aether.collection.DependencyGraphTransformer;
40  import org.eclipse.aether.collection.DependencyTraverser;
41  import org.eclipse.aether.collection.VersionFilter;
42  import org.eclipse.aether.graph.DefaultDependencyNode;
43  import org.eclipse.aether.graph.Dependency;
44  import org.eclipse.aether.graph.DependencyNode;
45  import org.eclipse.aether.impl.ArtifactDescriptorReader;
46  import org.eclipse.aether.impl.DependencyCollector;
47  import org.eclipse.aether.impl.RemoteRepositoryManager;
48  import org.eclipse.aether.impl.VersionRangeResolver;
49  import org.eclipse.aether.impl.scope.InternalScopeManager;
50  import org.eclipse.aether.internal.impl.Utils;
51  import org.eclipse.aether.repository.ArtifactRepository;
52  import org.eclipse.aether.repository.RemoteRepository;
53  import org.eclipse.aether.resolution.ArtifactDescriptorException;
54  import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
55  import org.eclipse.aether.resolution.ArtifactDescriptorResult;
56  import org.eclipse.aether.resolution.VersionRangeRequest;
57  import org.eclipse.aether.resolution.VersionRangeResolutionException;
58  import org.eclipse.aether.resolution.VersionRangeResult;
59  import org.eclipse.aether.scope.ResolutionScope;
60  import org.eclipse.aether.scope.SystemDependencyScope;
61  import org.eclipse.aether.spi.artifact.decorator.ArtifactDecorator;
62  import org.eclipse.aether.spi.artifact.decorator.ArtifactDecoratorFactory;
63  import org.eclipse.aether.util.ConfigUtils;
64  import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
65  import org.eclipse.aether.version.Version;
66  import org.slf4j.Logger;
67  import org.slf4j.LoggerFactory;
68  
69  import static java.util.Objects.requireNonNull;
70  
71  /**
72   * Helper class for delegate implementations, they MUST subclass this class.
73   *
74   * @since 1.8.0
75   */
76  public abstract class DependencyCollectorDelegate implements DependencyCollector {
77      /**
78       * Only exceptions up to the number given in this configuration property are emitted. Exceptions which exceed
79       * that number are swallowed.
80       *
81       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
82       * @configurationType {@link java.lang.Integer}
83       * @configurationDefaultValue {@link #DEFAULT_MAX_EXCEPTIONS}
84       */
85      public static final String CONFIG_PROP_MAX_EXCEPTIONS =
86              DefaultDependencyCollector.CONFIG_PROPS_PREFIX + "maxExceptions";
87  
88      public static final int DEFAULT_MAX_EXCEPTIONS = 50;
89  
90      /**
91       * Only up to the given amount cyclic dependencies are emitted.
92       *
93       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
94       * @configurationType {@link java.lang.Integer}
95       * @configurationDefaultValue {@link #DEFAULT_MAX_CYCLES}
96       */
97      public static final String CONFIG_PROP_MAX_CYCLES = DefaultDependencyCollector.CONFIG_PROPS_PREFIX + "maxCycles";
98  
99      public static final int DEFAULT_MAX_CYCLES = 10;
100 
101     /**
102      * The allowed runs of collection (and re-collection). By default, there must be at least 1 run, so values smaller
103      * than 1 are considered configuration errors and will fail dependency collection.
104      *
105      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
106      * @configurationType {@link java.lang.Integer}
107      * @configurationDefaultValue {@link #DEFAULT_MAX_RUNS}
108      * @since 2.0.19
109      */
110     public static final String CONFIG_PROP_MAX_RUNS = DefaultDependencyCollector.CONFIG_PROPS_PREFIX + "maxRuns";
111 
112     public static final int DEFAULT_MAX_RUNS = 5;
113 
114     protected final Logger logger = LoggerFactory.getLogger(getClass());
115 
116     protected final RemoteRepositoryManager remoteRepositoryManager;
117 
118     protected final ArtifactDescriptorReader descriptorReader;
119 
120     protected final VersionRangeResolver versionRangeResolver;
121 
122     protected final Map<String, ArtifactDecoratorFactory> artifactDecoratorFactories;
123 
124     protected DependencyCollectorDelegate(
125             RemoteRepositoryManager remoteRepositoryManager,
126             ArtifactDescriptorReader artifactDescriptorReader,
127             VersionRangeResolver versionRangeResolver,
128             Map<String, ArtifactDecoratorFactory> artifactDecoratorFactories) {
129         this.remoteRepositoryManager =
130                 requireNonNull(remoteRepositoryManager, "remote repository manager cannot be null");
131         this.descriptorReader = requireNonNull(artifactDescriptorReader, "artifact descriptor reader cannot be null");
132         this.versionRangeResolver = requireNonNull(versionRangeResolver, "version range resolver cannot be null");
133         this.artifactDecoratorFactories =
134                 requireNonNull(artifactDecoratorFactories, "artifact decorator factories cannot be null");
135     }
136 
137     @SuppressWarnings("checkstyle:methodlength")
138     @Override
139     public final CollectResult collectDependencies(
140             final RepositorySystemSession originalSession, final CollectRequest request)
141             throws DependencyCollectionException {
142         requireNonNull(originalSession, "session cannot be null");
143         requireNonNull(request, "request cannot be null");
144 
145         final InternalScopeManager scopeManager = (InternalScopeManager) originalSession.getScopeManager();
146         final RepositorySystemSession setUpSession = setUpSession(originalSession, request, scopeManager);
147         final DependencyCollectionChecker dependencyCollectionChecker =
148                 originalSession.getDependencyCollectionChecker() == null
149                         ? DependencyCollectionChecker.NOOP
150                         : originalSession.getDependencyCollectionChecker();
151 
152         final RequestTrace trace = RequestTrace.newChild(request.getTrace(), request);
153 
154         final Map<String, Object> stats = new LinkedHashMap<>();
155         final AtomicInteger runs = new AtomicInteger(0);
156         final int maxRuns = ConfigUtils.getInteger(originalSession, DEFAULT_MAX_RUNS, CONFIG_PROP_MAX_RUNS);
157         if (maxRuns < 1) {
158             throw new DependencyCollectionException(
159                     new CollectResult(request),
160                     "Invalid configuration: '" + CONFIG_PROP_MAX_RUNS
161                             + "' configuration must be equal or grater than 1");
162         }
163 
164         CollectResult result = null;
165 
166         boolean finished = false;
167         while (!finished) {
168             final long time1 = System.nanoTime();
169             if (runs.incrementAndGet() > maxRuns) {
170                 throw new DependencyCollectionException(
171                         new CollectResult(request),
172                         "Too many collection attempts (bug of used DependencyCollectionChecker?)");
173             }
174 
175             RepositorySystemSession session = dependencyCollectionChecker.prepare(setUpSession, request);
176             final DependencyTraverser depTraverser = session.getDependencyTraverser();
177             final VersionFilter verFilter = session.getVersionFilter();
178 
179             Dependency root = request.getRoot();
180             List<RemoteRepository> repositories = request.getRepositories();
181             List<Dependency> dependencies = request.getDependencies();
182             List<Dependency> managedDependencies = request.getManagedDependencies();
183 
184             result = new CollectResult(request);
185             DefaultDependencyNode node;
186             if (root != null) {
187                 List<? extends Version> versions;
188                 VersionRangeResult rangeResult;
189                 try {
190                     VersionRangeRequest rangeRequest = new VersionRangeRequest(
191                             root.getArtifact(), request.getRepositories(), request.getRequestContext());
192                     rangeRequest.setTrace(trace);
193                     rangeResult = versionRangeResolver.resolveVersionRange(session, rangeRequest);
194                     versions = filterVersions(root, rangeResult, verFilter, new DefaultVersionFilterContext(session));
195                 } catch (VersionRangeResolutionException e) {
196                     result.addException(e);
197                     throw new DependencyCollectionException(result, e.getMessage());
198                 }
199 
200                 Version version = versions.get(versions.size() - 1);
201                 root = root.setArtifact(root.getArtifact().setVersion(version.toString()));
202 
203                 ArtifactDescriptorResult descriptorResult;
204                 try {
205                     ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
206                     descriptorRequest.setArtifact(root.getArtifact());
207                     descriptorRequest.setRepositories(request.getRepositories());
208                     descriptorRequest.setRequestContext(request.getRequestContext());
209                     descriptorRequest.setTrace(trace);
210                     if (isLackingDescriptor(session, root.getArtifact())) {
211                         descriptorResult = new ArtifactDescriptorResult(descriptorRequest);
212                     } else {
213                         descriptorResult = descriptorReader.readArtifactDescriptor(session, descriptorRequest);
214                         for (ArtifactDecorator decorator :
215                                 Utils.getArtifactDecorators(session, artifactDecoratorFactories)) {
216                             descriptorResult.setArtifact(decorator.decorateArtifact(descriptorResult));
217                         }
218                     }
219                 } catch (ArtifactDescriptorException e) {
220                     result.addException(e);
221                     throw new DependencyCollectionException(result, e.getMessage());
222                 }
223 
224                 root = root.setArtifact(descriptorResult.getArtifact());
225 
226                 if (!session.isIgnoreArtifactDescriptorRepositories()) {
227                     repositories = remoteRepositoryManager.aggregateRepositories(
228                             session, repositories, descriptorResult.getRepositories(), true);
229                 }
230                 dependencies = mergeDeps(dependencies, descriptorResult.getDependencies());
231                 managedDependencies = mergeDeps(managedDependencies, descriptorResult.getManagedDependencies());
232 
233                 node = new DefaultDependencyNode(root);
234                 node.setRequestContext(request.getRequestContext());
235                 node.setRelocations(descriptorResult.getRelocations());
236                 node.setVersionConstraint(rangeResult.getVersionConstraint());
237                 node.setVersion(version);
238                 node.setAliases(descriptorResult.getAliases());
239                 node.setRepositories(request.getRepositories());
240             } else {
241                 node = new DefaultDependencyNode(request.getRootArtifact());
242                 node.setRequestContext(request.getRequestContext());
243                 node.setRepositories(request.getRepositories());
244             }
245 
246             result.setRoot(node);
247 
248             boolean traverse = root == null || depTraverser == null || depTraverser.traverseDependency(root);
249             String errorPath = null;
250             if (traverse && !dependencies.isEmpty()) {
251                 DataPool pool = new DataPool(session);
252 
253                 DefaultDependencyCollectionContext context = new DefaultDependencyCollectionContext(
254                         session, request.getRootArtifact(), root, managedDependencies);
255 
256                 DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext(session);
257 
258                 Results results = new Results(result, session);
259 
260                 doCollectDependencies(
261                         session,
262                         trace,
263                         pool,
264                         context,
265                         versionContext,
266                         request,
267                         node,
268                         repositories,
269                         dependencies,
270                         managedDependencies,
271                         results);
272 
273                 errorPath = results.getErrorPath();
274             }
275 
276             final long time2 = System.nanoTime();
277 
278             DependencyGraphTransformer transformer = session.getDependencyGraphTransformer();
279             if (transformer != null) {
280                 try {
281                     DefaultDependencyGraphTransformationContext context =
282                             new DefaultDependencyGraphTransformationContext(session);
283                     context.put(TransformationContextKeys.STATS, stats);
284                     result.setRoot(transformer.transformGraph(node, context));
285                 } catch (RepositoryException e) {
286                     result.addException(e);
287                 }
288             }
289 
290             if (errorPath != null) {
291                 throw new DependencyCollectionException(result, "Failed to collect dependencies at " + errorPath);
292             }
293             if (!result.getExceptions().isEmpty()) {
294                 throw new DependencyCollectionException(result);
295             }
296 
297             if (request.getResolutionScope() != null) {
298                 result = scopeManager.postProcess(session, request.getResolutionScope(), result);
299             }
300 
301             long time3 = System.nanoTime();
302             stats.put(getClass().getSimpleName() + ".collectTime", time2 - time1);
303             stats.put(getClass().getSimpleName() + ".transformTime", time3 - time2);
304 
305             finished = dependencyCollectionChecker.isSatisfactory(session, request, result);
306         }
307 
308         stats.put(getClass().getSimpleName() + ".runs", runs.get());
309         if (logger.isDebugEnabled()) {
310             logger.debug("Dependency collection stats {}", stats);
311         }
312 
313         return result;
314     }
315 
316     /**
317      * Creates child {@link RequestTrace} instance from passed in {@link RequestTrace} and parameters by creating
318      * {@link CollectStepDataImpl} instance out of passed in data. Caller must ensure that passed in parameters are
319      * NOT affected by threading (or that there is no multi threading involved). In other words, the passed in values
320      * should be immutable.
321      *
322      * @param trace   The current trace instance.
323      * @param context The context from {@link CollectRequest#getRequestContext()}, never {@code null}.
324      * @param path    List representing the path of dependency nodes, never {@code null}. Caller must ensure, that this
325      *                list does not change during the lifetime of the requested {@link RequestTrace} instance. If it may
326      *                change, simplest is to pass here a copy of used list.
327      * @param node    Currently collected node, that collector came by following the passed in path.
328      * @return A child request trance instance, never {@code null}.
329      */
330     protected RequestTrace collectStepTrace(
331             RequestTrace trace, String context, List<DependencyNode> path, Dependency node) {
332         return RequestTrace.newChild(trace, new CollectStepDataImpl(context, path, node));
333     }
334 
335     @SuppressWarnings("checkstyle:parameternumber")
336     protected abstract void doCollectDependencies(
337             RepositorySystemSession session,
338             RequestTrace trace,
339             DataPool pool,
340             DefaultDependencyCollectionContext context,
341             DefaultVersionFilterContext versionContext,
342             CollectRequest request,
343             DependencyNode node,
344             List<RemoteRepository> repositories,
345             List<Dependency> dependencies,
346             List<Dependency> managedDependencies,
347             Results results)
348             throws DependencyCollectionException;
349 
350     protected RepositorySystemSession setUpSession(
351             RepositorySystemSession session, CollectRequest collectRequest, InternalScopeManager scopeManager) {
352         DefaultRepositorySystemSession optimized = new DefaultRepositorySystemSession(session);
353         optimized.setArtifactTypeRegistry(CachingArtifactTypeRegistry.newInstance(session));
354 
355         ResolutionScope resolutionScope = collectRequest.getResolutionScope();
356         if (resolutionScope != null) {
357             requireNonNull(scopeManager, "ScopeManager is not set on session");
358             optimized.setDependencySelector(scopeManager.getDependencySelector(session, resolutionScope));
359         }
360         return optimized;
361     }
362 
363     protected List<Dependency> mergeDeps(List<Dependency> dominant, List<Dependency> recessive) {
364         List<Dependency> result;
365         if (dominant == null || dominant.isEmpty()) {
366             result = recessive;
367         } else if (recessive == null || recessive.isEmpty()) {
368             result = dominant;
369         } else {
370             int initialCapacity = dominant.size() + recessive.size();
371             result = new ArrayList<>(initialCapacity);
372             Collection<String> ids = new HashSet<>(initialCapacity, 1.0f);
373             for (Dependency dependency : dominant) {
374                 ids.add(getId(dependency.getArtifact()));
375                 result.add(dependency);
376             }
377             for (Dependency dependency : recessive) {
378                 if (!ids.contains(getId(dependency.getArtifact()))) {
379                     result.add(dependency);
380                 }
381             }
382         }
383         return result;
384     }
385 
386     protected static String getId(Artifact a) {
387         return a.getGroupId() + ':' + a.getArtifactId() + ':' + a.getClassifier() + ':' + a.getExtension();
388     }
389 
390     @SuppressWarnings("checkstyle:parameternumber")
391     protected static DefaultDependencyNode createDependencyNode(
392             List<Artifact> relocations,
393             PremanagedDependency preManaged,
394             VersionRangeResult rangeResult,
395             Version version,
396             Dependency d,
397             Collection<Artifact> aliases,
398             List<RemoteRepository> repos,
399             String requestContext) {
400         DefaultDependencyNode child = new DefaultDependencyNode(d);
401         preManaged.applyTo(child);
402         child.setRelocations(relocations);
403         child.setVersionConstraint(rangeResult.getVersionConstraint());
404         child.setVersion(version);
405         child.setAliases(aliases);
406         child.setRepositories(repos);
407         child.setRequestContext(requestContext);
408         return child;
409     }
410 
411     protected static DefaultDependencyNode createDependencyNode(
412             List<Artifact> relocations,
413             PremanagedDependency preManaged,
414             VersionRangeResult rangeResult,
415             Version version,
416             Dependency d,
417             ArtifactDescriptorResult descriptorResult,
418             DependencyNode cycleNode) {
419         DefaultDependencyNode child = createDependencyNode(
420                 relocations,
421                 preManaged,
422                 rangeResult,
423                 version,
424                 d,
425                 descriptorResult.getAliases(),
426                 cycleNode.getRepositories(),
427                 cycleNode.getRequestContext());
428         child.setChildren(cycleNode.getChildren());
429         return child;
430     }
431 
432     protected static ArtifactDescriptorRequest createArtifactDescriptorRequest(
433             String requestContext, RequestTrace requestTrace, List<RemoteRepository> repositories, Dependency d) {
434         ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
435         descriptorRequest.setArtifact(d.getArtifact());
436         descriptorRequest.setRepositories(repositories);
437         descriptorRequest.setRequestContext(requestContext);
438         descriptorRequest.setTrace(requestTrace);
439         return descriptorRequest;
440     }
441 
442     protected static VersionRangeRequest createVersionRangeRequest(
443             String requestContext,
444             RequestTrace requestTrace,
445             List<RemoteRepository> repositories,
446             Dependency dependency) {
447         VersionRangeRequest rangeRequest = new VersionRangeRequest();
448         rangeRequest.setArtifact(dependency.getArtifact());
449         rangeRequest.setRepositories(repositories);
450         rangeRequest.setRequestContext(requestContext);
451         rangeRequest.setTrace(requestTrace);
452         return rangeRequest;
453     }
454 
455     protected VersionRangeResult cachedResolveRangeResult(
456             VersionRangeRequest rangeRequest, DataPool pool, RepositorySystemSession session)
457             throws VersionRangeResolutionException {
458         Object key = pool.toKey(rangeRequest);
459         VersionRangeResult rangeResult = pool.getConstraint(key, rangeRequest);
460         if (rangeResult == null) {
461             rangeResult = versionRangeResolver.resolveVersionRange(session, rangeRequest);
462             pool.putConstraint(key, rangeResult);
463         }
464         return rangeResult;
465     }
466 
467     protected static boolean isLackingDescriptor(RepositorySystemSession session, Artifact artifact) {
468         SystemDependencyScope systemDependencyScope = session.getSystemDependencyScope();
469         return systemDependencyScope != null && systemDependencyScope.getSystemPath(artifact) != null;
470     }
471 
472     protected static List<RemoteRepository> getRemoteRepositories(
473             ArtifactRepository repository, List<RemoteRepository> repositories) {
474         if (repository instanceof RemoteRepository) {
475             return Collections.singletonList((RemoteRepository) repository);
476         }
477         if (repository != null) {
478             return Collections.emptyList();
479         }
480         return repositories;
481     }
482 
483     protected static List<? extends Version> filterVersions(
484             Dependency dependency,
485             VersionRangeResult rangeResult,
486             VersionFilter verFilter,
487             DefaultVersionFilterContext verContext)
488             throws VersionRangeResolutionException {
489         if (rangeResult.getVersions().isEmpty()) {
490             throw new VersionRangeResolutionException(
491                     rangeResult, "No versions available for " + dependency.getArtifact() + " within specified range");
492         }
493 
494         List<? extends Version> versions;
495         if (verFilter != null && rangeResult.getVersionConstraint().getRange() != null) {
496             verContext = verContext.initialize(dependency, rangeResult);
497             try {
498                 verFilter.filterVersions(verContext);
499             } catch (RepositoryException e) {
500                 throw new VersionRangeResolutionException(
501                         rangeResult, "Failed to filter versions for " + dependency.getArtifact(), e);
502             }
503             versions = verContext.get();
504             if (versions.isEmpty()) {
505                 throw new VersionRangeResolutionException(
506                         rangeResult,
507                         "No acceptable versions for " + dependency.getArtifact() + ": " + rangeResult.getVersions());
508             }
509         } else {
510             versions = rangeResult.getVersions();
511         }
512         return versions;
513     }
514 
515     protected ArtifactDescriptorResult resolveCachedArtifactDescriptor(
516             DataPool pool,
517             ArtifactDescriptorRequest descriptorRequest,
518             RepositorySystemSession session,
519             Dependency d,
520             Results results,
521             List<DependencyNode> nodes) {
522         DataPool.DescriptorKey key = pool.toKey(descriptorRequest);
523         ArtifactDescriptorResult descriptorResult = pool.getDescriptor(key, descriptorRequest);
524         if (descriptorResult == null) {
525             try {
526                 descriptorResult = descriptorReader.readArtifactDescriptor(session, descriptorRequest);
527                 for (ArtifactDecorator decorator : Utils.getArtifactDecorators(session, artifactDecoratorFactories)) {
528                     descriptorResult.setArtifact(decorator.decorateArtifact(descriptorResult));
529                 }
530                 pool.putDescriptor(key, descriptorResult);
531             } catch (ArtifactDescriptorException e) {
532                 results.addException(d, e, nodes);
533                 pool.putDescriptor(key, e);
534                 return null;
535             }
536         } else if (descriptorResult == DataPool.NO_DESCRIPTOR) {
537             return null;
538         }
539         return descriptorResult;
540     }
541 
542     /**
543      * Helper class used during collection.
544      */
545     protected static class Results {
546 
547         private final CollectResult result;
548 
549         final int maxExceptions;
550 
551         final int maxCycles;
552 
553         volatile String errorPath;
554 
555         public Results(CollectResult result, RepositorySystemSession session) {
556             this.result = result;
557 
558             maxExceptions = ConfigUtils.getInteger(session, DEFAULT_MAX_EXCEPTIONS, CONFIG_PROP_MAX_EXCEPTIONS);
559 
560             maxCycles = ConfigUtils.getInteger(session, DEFAULT_MAX_CYCLES, CONFIG_PROP_MAX_CYCLES);
561         }
562 
563         public synchronized CollectResult getResult() {
564             return result;
565         }
566 
567         public synchronized String getErrorPath() {
568             return errorPath;
569         }
570 
571         public synchronized void addException(Dependency dependency, Exception e, List<DependencyNode> nodes) {
572             if (maxExceptions < 0 || result.getExceptions().size() < maxExceptions) {
573                 result.addException(e);
574                 if (errorPath == null) {
575                     StringBuilder buffer = new StringBuilder(256);
576                     for (DependencyNode node : nodes) {
577                         if (buffer.length() > 0) {
578                             buffer.append(" -> ");
579                         }
580                         Dependency dep = node.getDependency();
581                         if (dep != null) {
582                             buffer.append(dep.getArtifact());
583                         }
584                     }
585                     if (buffer.length() > 0) {
586                         buffer.append(" -> ");
587                     }
588                     buffer.append(dependency.getArtifact());
589                     errorPath = buffer.toString();
590                 }
591             }
592         }
593 
594         public synchronized void addCycle(List<DependencyNode> nodes, int cycleEntry, Dependency dependency) {
595             if (maxCycles < 0 || result.getCycles().size() < maxCycles) {
596                 result.addCycle(new DefaultDependencyCycle(nodes, cycleEntry, dependency));
597             }
598         }
599     }
600 }