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.internal.impl.collect;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashSet;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.atomic.AtomicInteger;
029
030import org.eclipse.aether.DefaultRepositorySystemSession;
031import org.eclipse.aether.RepositoryException;
032import org.eclipse.aether.RepositorySystemSession;
033import org.eclipse.aether.RequestTrace;
034import org.eclipse.aether.artifact.Artifact;
035import org.eclipse.aether.collection.CollectRequest;
036import org.eclipse.aether.collection.CollectResult;
037import org.eclipse.aether.collection.DependencyCollectionChecker;
038import org.eclipse.aether.collection.DependencyCollectionException;
039import org.eclipse.aether.collection.DependencyGraphTransformer;
040import org.eclipse.aether.collection.DependencyTraverser;
041import org.eclipse.aether.collection.VersionFilter;
042import org.eclipse.aether.graph.DefaultDependencyNode;
043import org.eclipse.aether.graph.Dependency;
044import org.eclipse.aether.graph.DependencyNode;
045import org.eclipse.aether.impl.ArtifactDescriptorReader;
046import org.eclipse.aether.impl.DependencyCollector;
047import org.eclipse.aether.impl.RemoteRepositoryManager;
048import org.eclipse.aether.impl.VersionRangeResolver;
049import org.eclipse.aether.impl.scope.InternalScopeManager;
050import org.eclipse.aether.internal.impl.Utils;
051import org.eclipse.aether.repository.ArtifactRepository;
052import org.eclipse.aether.repository.RemoteRepository;
053import org.eclipse.aether.resolution.ArtifactDescriptorException;
054import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
055import org.eclipse.aether.resolution.ArtifactDescriptorResult;
056import org.eclipse.aether.resolution.VersionRangeRequest;
057import org.eclipse.aether.resolution.VersionRangeResolutionException;
058import org.eclipse.aether.resolution.VersionRangeResult;
059import org.eclipse.aether.scope.ResolutionScope;
060import org.eclipse.aether.scope.SystemDependencyScope;
061import org.eclipse.aether.spi.artifact.decorator.ArtifactDecorator;
062import org.eclipse.aether.spi.artifact.decorator.ArtifactDecoratorFactory;
063import org.eclipse.aether.util.ConfigUtils;
064import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
065import org.eclipse.aether.version.Version;
066import org.slf4j.Logger;
067import org.slf4j.LoggerFactory;
068
069import static java.util.Objects.requireNonNull;
070
071/**
072 * Helper class for delegate implementations, they MUST subclass this class.
073 *
074 * @since 1.8.0
075 */
076public abstract class DependencyCollectorDelegate implements DependencyCollector {
077    /**
078     * Only exceptions up to the number given in this configuration property are emitted. Exceptions which exceed
079     * that number are swallowed.
080     *
081     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
082     * @configurationType {@link java.lang.Integer}
083     * @configurationDefaultValue {@link #DEFAULT_MAX_EXCEPTIONS}
084     */
085    public static final String CONFIG_PROP_MAX_EXCEPTIONS =
086            DefaultDependencyCollector.CONFIG_PROPS_PREFIX + "maxExceptions";
087
088    public static final int DEFAULT_MAX_EXCEPTIONS = 50;
089
090    /**
091     * Only up to the given amount cyclic dependencies are emitted.
092     *
093     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
094     * @configurationType {@link java.lang.Integer}
095     * @configurationDefaultValue {@link #DEFAULT_MAX_CYCLES}
096     */
097    public static final String CONFIG_PROP_MAX_CYCLES = DefaultDependencyCollector.CONFIG_PROPS_PREFIX + "maxCycles";
098
099    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}