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;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.List;
030import java.util.Map;
031import java.util.Objects;
032import java.util.concurrent.atomic.AtomicBoolean;
033import java.util.concurrent.atomic.AtomicInteger;
034import java.util.function.Consumer;
035import java.util.stream.Collectors;
036
037import org.eclipse.aether.ConfigurationProperties;
038import org.eclipse.aether.RepositorySystem;
039import org.eclipse.aether.RepositorySystemSession;
040import org.eclipse.aether.RequestTrace;
041import org.eclipse.aether.SyncContext;
042import org.eclipse.aether.artifact.Artifact;
043import org.eclipse.aether.collection.CollectRequest;
044import org.eclipse.aether.collection.CollectResult;
045import org.eclipse.aether.collection.DependencyCollectionException;
046import org.eclipse.aether.deployment.DeployRequest;
047import org.eclipse.aether.deployment.DeployResult;
048import org.eclipse.aether.deployment.DeploymentException;
049import org.eclipse.aether.graph.DependencyFilter;
050import org.eclipse.aether.graph.DependencyNode;
051import org.eclipse.aether.graph.DependencyVisitor;
052import org.eclipse.aether.impl.ArtifactDescriptorReader;
053import org.eclipse.aether.impl.ArtifactResolver;
054import org.eclipse.aether.impl.DependencyCollector;
055import org.eclipse.aether.impl.Deployer;
056import org.eclipse.aether.impl.Installer;
057import org.eclipse.aether.impl.LocalRepositoryProvider;
058import org.eclipse.aether.impl.MetadataResolver;
059import org.eclipse.aether.impl.RemoteRepositoryManager;
060import org.eclipse.aether.impl.RepositorySystemLifecycle;
061import org.eclipse.aether.impl.RepositorySystemValidator;
062import org.eclipse.aether.impl.VersionRangeResolver;
063import org.eclipse.aether.impl.VersionResolver;
064import org.eclipse.aether.installation.InstallRequest;
065import org.eclipse.aether.installation.InstallResult;
066import org.eclipse.aether.installation.InstallationException;
067import org.eclipse.aether.internal.impl.session.DefaultSessionBuilder;
068import org.eclipse.aether.repository.Authentication;
069import org.eclipse.aether.repository.LocalRepository;
070import org.eclipse.aether.repository.LocalRepositoryManager;
071import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
072import org.eclipse.aether.repository.Proxy;
073import org.eclipse.aether.repository.RemoteRepository;
074import org.eclipse.aether.resolution.ArtifactDescriptorException;
075import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
076import org.eclipse.aether.resolution.ArtifactDescriptorResult;
077import org.eclipse.aether.resolution.ArtifactRequest;
078import org.eclipse.aether.resolution.ArtifactResolutionException;
079import org.eclipse.aether.resolution.ArtifactResult;
080import org.eclipse.aether.resolution.DependencyRequest;
081import org.eclipse.aether.resolution.DependencyResolutionException;
082import org.eclipse.aether.resolution.DependencyResult;
083import org.eclipse.aether.resolution.MetadataRequest;
084import org.eclipse.aether.resolution.MetadataResult;
085import org.eclipse.aether.resolution.VersionRangeRequest;
086import org.eclipse.aether.resolution.VersionRangeResolutionException;
087import org.eclipse.aether.resolution.VersionRangeResult;
088import org.eclipse.aether.resolution.VersionRequest;
089import org.eclipse.aether.resolution.VersionResolutionException;
090import org.eclipse.aether.resolution.VersionResult;
091import org.eclipse.aether.spi.artifact.decorator.ArtifactDecorator;
092import org.eclipse.aether.spi.artifact.decorator.ArtifactDecoratorFactory;
093import org.eclipse.aether.spi.synccontext.SyncContextFactory;
094import org.eclipse.aether.util.ConfigUtils;
095import org.eclipse.aether.util.graph.visitor.LevelOrderDependencyNodeConsumerVisitor;
096import org.eclipse.aether.util.graph.visitor.PostorderDependencyNodeConsumerVisitor;
097import org.eclipse.aether.util.graph.visitor.PreorderDependencyNodeConsumerVisitor;
098import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager;
099
100import static java.util.Objects.requireNonNull;
101import static java.util.stream.Collectors.toList;
102
103/**
104 *
105 */
106@Singleton
107@Named
108public class DefaultRepositorySystem implements RepositorySystem {
109    /**
110     * Sentinel object placed into the {@link RequestTrace} chain by each public method
111     * to detect re-entrant calls into this {@code RepositorySystem}.
112     * <p>
113     * The resolver's architecture assumes that internal components ({@code ArtifactResolver},
114     * {@code DependencyCollector}, {@code ArtifactDescriptorReader}, etc.) call each other
115     * directly via the internal API in {@code org.eclipse.aether.impl}, never going through
116     * the public {@code RepositorySystem} facade. This means session validation, request
117     * validation, and artifact decoration only need to run once — on the outermost call.
118     * <p>
119     * However, some {@code RepositorySystem} consumers (notably Maven 4's
120     * {@code ArtifactDescriptorReader} → {@code ModelBuilder} → {@code ModelResolver} chain)
121     * re-enter {@code RepositorySystem} during an ongoing operation. Without this guard,
122     * every re-entrant call redundantly validates the session, validates the request (which
123     * may reject intermediate state like uninterpolated expressions from transitive POMs),
124     * and applies artifact decorators. This causes both correctness issues (false validation
125     * failures) and unnecessary performance overhead.
126     * <p>
127     * Re-entrancy can happen on the <em>same</em> thread (e.g. {@code collectDependencies}
128     * → {@code readArtifactDescriptor} → model builder → model resolver → {@code resolveVersionRange})
129     * or on a <em>different</em> thread when the resolver uses internal parallelism (e.g.
130     * {@code BfDependencyCollector}'s {@code SmartExecutor} dispatches descriptor resolution
131     * to pool threads, which then re-enter {@code RepositorySystem} via the model resolver).
132     * <p>
133     * To handle both cases, we leverage the {@link RequestTrace} chain that is already
134     * propagated across threads by the caller (e.g. Maven's model builder explicitly copies
135     * traces to pool threads). On the outermost call, we stamp this marker into the request's
136     * trace. On re-entry — whether on the same thread or a pool thread — the marker is found
137     * in the trace ancestry, so validation and decoration are skipped.
138     */
139    private static final Object REPOSITORY_SYSTEM_CALL = new Object() {
140        @Override
141        public String toString() {
142            return "RepositorySystem";
143        }
144    };
145
146    private final AtomicBoolean shutdown;
147
148    private final AtomicInteger sessionIdCounter;
149
150    private final VersionResolver versionResolver;
151
152    private final VersionRangeResolver versionRangeResolver;
153
154    private final ArtifactResolver artifactResolver;
155
156    private final MetadataResolver metadataResolver;
157
158    private final ArtifactDescriptorReader artifactDescriptorReader;
159
160    private final DependencyCollector dependencyCollector;
161
162    private final Installer installer;
163
164    private final Deployer deployer;
165
166    private final LocalRepositoryProvider localRepositoryProvider;
167
168    private final SyncContextFactory syncContextFactory;
169
170    private final RemoteRepositoryManager remoteRepositoryManager;
171
172    private final RepositorySystemLifecycle repositorySystemLifecycle;
173
174    private final Map<String, ArtifactDecoratorFactory> artifactDecoratorFactories;
175
176    private final RepositorySystemValidator repositorySystemValidator;
177
178    @SuppressWarnings("checkstyle:parameternumber")
179    @Inject
180    public DefaultRepositorySystem(
181            VersionResolver versionResolver,
182            VersionRangeResolver versionRangeResolver,
183            ArtifactResolver artifactResolver,
184            MetadataResolver metadataResolver,
185            ArtifactDescriptorReader artifactDescriptorReader,
186            DependencyCollector dependencyCollector,
187            Installer installer,
188            Deployer deployer,
189            LocalRepositoryProvider localRepositoryProvider,
190            SyncContextFactory syncContextFactory,
191            RemoteRepositoryManager remoteRepositoryManager,
192            RepositorySystemLifecycle repositorySystemLifecycle,
193            Map<String, ArtifactDecoratorFactory> artifactDecoratorFactories,
194            RepositorySystemValidator repositorySystemValidator) {
195        this.shutdown = new AtomicBoolean(false);
196        this.sessionIdCounter = new AtomicInteger(0);
197        this.versionResolver = requireNonNull(versionResolver, "version resolver cannot be null");
198        this.versionRangeResolver = requireNonNull(versionRangeResolver, "version range resolver cannot be null");
199        this.artifactResolver = requireNonNull(artifactResolver, "artifact resolver cannot be null");
200        this.metadataResolver = requireNonNull(metadataResolver, "metadata resolver cannot be null");
201        this.artifactDescriptorReader =
202                requireNonNull(artifactDescriptorReader, "artifact descriptor reader cannot be null");
203        this.dependencyCollector = requireNonNull(dependencyCollector, "dependency collector cannot be null");
204        this.installer = requireNonNull(installer, "installer cannot be null");
205        this.deployer = requireNonNull(deployer, "deployer cannot be null");
206        this.localRepositoryProvider =
207                requireNonNull(localRepositoryProvider, "local repository provider cannot be null");
208        this.syncContextFactory = requireNonNull(syncContextFactory, "sync context factory cannot be null");
209        this.remoteRepositoryManager =
210                requireNonNull(remoteRepositoryManager, "remote repository provider cannot be null");
211        this.repositorySystemLifecycle =
212                requireNonNull(repositorySystemLifecycle, "repository system lifecycle cannot be null");
213        this.artifactDecoratorFactories =
214                requireNonNull(artifactDecoratorFactories, "artifact decorator factories cannot be null");
215        this.repositorySystemValidator =
216                requireNonNull(repositorySystemValidator, "repository system validator cannot be null");
217    }
218
219    @Override
220    public VersionResult resolveVersion(RepositorySystemSession session, VersionRequest request)
221            throws VersionResolutionException {
222        requireNonNull(request, "request cannot be null");
223        Runnable exitGuard = null;
224        if (!isReentrant(request.getTrace(), session)) {
225            validateSession(session);
226            repositorySystemValidator.validateVersionRequest(session, request);
227            request.setTrace(stampReentrancyMarker(request.getTrace()));
228            exitGuard = enterSessionScope(session);
229        }
230        try {
231            return versionResolver.resolveVersion(session, request);
232        } finally {
233            if (exitGuard != null) {
234                exitGuard.run();
235            }
236        }
237    }
238
239    @Override
240    public VersionRangeResult resolveVersionRange(RepositorySystemSession session, VersionRangeRequest request)
241            throws VersionRangeResolutionException {
242        requireNonNull(request, "request cannot be null");
243        Runnable exitGuard = null;
244        if (!isReentrant(request.getTrace(), session)) {
245            validateSession(session);
246            repositorySystemValidator.validateVersionRangeRequest(session, request);
247            request.setTrace(stampReentrancyMarker(request.getTrace()));
248            exitGuard = enterSessionScope(session);
249        }
250        try {
251            return versionRangeResolver.resolveVersionRange(session, request);
252        } finally {
253            if (exitGuard != null) {
254                exitGuard.run();
255            }
256        }
257    }
258
259    @Override
260    public ArtifactDescriptorResult readArtifactDescriptor(
261            RepositorySystemSession session, ArtifactDescriptorRequest request) throws ArtifactDescriptorException {
262        requireNonNull(request, "request cannot be null");
263        boolean outermost = !isReentrant(request.getTrace(), session);
264        Runnable exitGuard = null;
265        if (outermost) {
266            validateSession(session);
267            repositorySystemValidator.validateArtifactDescriptorRequest(session, request);
268            request.setTrace(stampReentrancyMarker(request.getTrace()));
269            exitGuard = enterSessionScope(session);
270        }
271        try {
272            ArtifactDescriptorResult descriptorResult =
273                    artifactDescriptorReader.readArtifactDescriptor(session, request);
274            if (outermost) {
275                for (ArtifactDecorator decorator : Utils.getArtifactDecorators(session, artifactDecoratorFactories)) {
276                    descriptorResult.setArtifact(decorator.decorateArtifact(descriptorResult));
277                }
278            }
279            return descriptorResult;
280        } finally {
281            if (exitGuard != null) {
282                exitGuard.run();
283            }
284        }
285    }
286
287    @Override
288    public ArtifactResult resolveArtifact(RepositorySystemSession session, ArtifactRequest request)
289            throws ArtifactResolutionException {
290        requireNonNull(request, "request cannot be null");
291        Runnable exitGuard = null;
292        if (!isReentrant(request.getTrace(), session)) {
293            validateSession(session);
294            repositorySystemValidator.validateArtifactRequests(session, Collections.singleton(request));
295            request.setTrace(stampReentrancyMarker(request.getTrace()));
296            exitGuard = enterSessionScope(session);
297        }
298        try {
299            return artifactResolver.resolveArtifact(session, request);
300        } finally {
301            if (exitGuard != null) {
302                exitGuard.run();
303            }
304        }
305    }
306
307    @Override
308    public List<ArtifactResult> resolveArtifacts(
309            RepositorySystemSession session, Collection<? extends ArtifactRequest> requests)
310            throws ArtifactResolutionException {
311        requireNonNull(requests, "requests cannot be null");
312        // All requests in a batch share the same trace context, so checking any one is sufficient.
313        RequestTrace firstTrace = requests.stream()
314                .map(ArtifactRequest::getTrace)
315                .filter(Objects::nonNull)
316                .findFirst()
317                .orElse(null);
318        Runnable exitGuard = null;
319        if (!isReentrant(firstTrace, session)) {
320            validateSession(session);
321            repositorySystemValidator.validateArtifactRequests(session, requests);
322            for (ArtifactRequest request : requests) {
323                request.setTrace(stampReentrancyMarker(request.getTrace()));
324            }
325            exitGuard = enterSessionScope(session);
326        }
327        try {
328            return artifactResolver.resolveArtifacts(session, requests);
329        } finally {
330            if (exitGuard != null) {
331                exitGuard.run();
332            }
333        }
334    }
335
336    @Override
337    public List<MetadataResult> resolveMetadata(
338            RepositorySystemSession session, Collection<? extends MetadataRequest> requests) {
339        requireNonNull(requests, "requests cannot be null");
340        // All requests in a batch share the same trace context, so checking any one is sufficient.
341        RequestTrace firstTrace = requests.stream()
342                .map(MetadataRequest::getTrace)
343                .filter(Objects::nonNull)
344                .findFirst()
345                .orElse(null);
346        Runnable exitGuard = null;
347        if (!isReentrant(firstTrace, session)) {
348            validateSession(session);
349            repositorySystemValidator.validateMetadataRequests(session, requests);
350            for (MetadataRequest request : requests) {
351                request.setTrace(stampReentrancyMarker(request.getTrace()));
352            }
353            exitGuard = enterSessionScope(session);
354        }
355        try {
356            return metadataResolver.resolveMetadata(session, requests);
357        } finally {
358            if (exitGuard != null) {
359                exitGuard.run();
360            }
361        }
362    }
363
364    @Override
365    public CollectResult collectDependencies(RepositorySystemSession session, CollectRequest request)
366            throws DependencyCollectionException {
367        requireNonNull(request, "request cannot be null");
368        Runnable exitGuard = null;
369        if (!isReentrant(request.getTrace(), session)) {
370            validateSession(session);
371            repositorySystemValidator.validateCollectRequest(session, request);
372            request.setTrace(stampReentrancyMarker(request.getTrace()));
373            exitGuard = enterSessionScope(session);
374        }
375        try {
376            return dependencyCollector.collectDependencies(session, request);
377        } finally {
378            if (exitGuard != null) {
379                exitGuard.run();
380            }
381        }
382    }
383
384    @Override
385    public DependencyResult resolveDependencies(RepositorySystemSession session, DependencyRequest request)
386            throws DependencyResolutionException {
387        requireNonNull(request, "request cannot be null");
388        Runnable exitGuard = null;
389        if (!isReentrant(request.getTrace(), session)) {
390            validateSession(session);
391            repositorySystemValidator.validateDependencyRequest(session, request);
392            request.setTrace(stampReentrancyMarker(request.getTrace()));
393            exitGuard = enterSessionScope(session);
394        }
395        try {
396            RequestTrace trace = RequestTrace.newChild(request.getTrace(), request);
397
398            DependencyResult result = new DependencyResult(request);
399
400            DependencyCollectionException dce = null;
401            ArtifactResolutionException are = null;
402
403            if (request.getRoot() != null) {
404                result.setRoot(request.getRoot());
405            } else if (request.getCollectRequest() != null) {
406                CollectResult collectResult;
407                try {
408                    request.getCollectRequest().setTrace(trace);
409                    collectResult = dependencyCollector.collectDependencies(session, request.getCollectRequest());
410                } catch (DependencyCollectionException e) {
411                    dce = e;
412                    collectResult = e.getResult();
413                }
414                result.setRoot(collectResult.getRoot());
415                result.setCycles(collectResult.getCycles());
416                result.setCollectExceptions(collectResult.getExceptions());
417            } else {
418                throw new NullPointerException("dependency node and collect request cannot be null");
419            }
420
421            final List<DependencyNode> dependencyNodes =
422                    doFlattenDependencyNodes(session, result.getRoot(), request.getFilter());
423
424            final List<ArtifactRequest> requests = dependencyNodes.stream()
425                    .map(n -> {
426                        if (n.getDependency() != null) {
427                            ArtifactRequest artifactRequest = new ArtifactRequest(n);
428                            artifactRequest.setTrace(trace);
429                            return artifactRequest;
430                        } else {
431                            return null;
432                        }
433                    })
434                    .filter(Objects::nonNull)
435                    .collect(Collectors.toList());
436            List<ArtifactResult> results;
437            try {
438                results = artifactResolver.resolveArtifacts(session, requests);
439            } catch (ArtifactResolutionException e) {
440                are = e;
441                results = e.getResults();
442            }
443            result.setDependencyNodeResults(dependencyNodes);
444            result.setArtifactResults(results);
445
446            updateNodesWithResolvedArtifacts(results);
447
448            if (dce != null) {
449                throw new DependencyResolutionException(result, dce);
450            } else if (are != null) {
451                throw new DependencyResolutionException(result, are);
452            }
453
454            return result;
455        } finally {
456            if (exitGuard != null) {
457                exitGuard.run();
458            }
459        }
460    }
461
462    @Override
463    public List<DependencyNode> flattenDependencyNodes(
464            RepositorySystemSession session, DependencyNode root, DependencyFilter dependencyFilter) {
465        validateSession(session);
466        requireNonNull(root, "root cannot be null");
467        return doFlattenDependencyNodes(session, root, dependencyFilter);
468    }
469
470    private List<DependencyNode> doFlattenDependencyNodes(
471            RepositorySystemSession session, DependencyNode root, DependencyFilter dependencyFilter) {
472        final ArrayList<DependencyNode> dependencyNodes = new ArrayList<>();
473        if (root != null) {
474            root.accept(getDependencyVisitor(session, dependencyNodes::add, dependencyFilter));
475        }
476        return dependencyNodes;
477    }
478
479    private DependencyVisitor getDependencyVisitor(
480            RepositorySystemSession session, Consumer<DependencyNode> nodeConsumer, DependencyFilter dependencyFilter) {
481        String strategy = ConfigUtils.getString(
482                session,
483                ConfigurationProperties.DEFAULT_REPOSITORY_SYSTEM_DEPENDENCY_VISITOR,
484                ConfigurationProperties.REPOSITORY_SYSTEM_DEPENDENCY_VISITOR);
485        switch (strategy) {
486            case PreorderDependencyNodeConsumerVisitor.NAME:
487                return new PreorderDependencyNodeConsumerVisitor(nodeConsumer, dependencyFilter);
488            case PostorderDependencyNodeConsumerVisitor.NAME:
489                return new PostorderDependencyNodeConsumerVisitor(nodeConsumer, dependencyFilter);
490            case LevelOrderDependencyNodeConsumerVisitor.NAME:
491                return new LevelOrderDependencyNodeConsumerVisitor(nodeConsumer, dependencyFilter);
492            default:
493                throw new IllegalArgumentException("Invalid dependency visitor strategy: " + strategy);
494        }
495    }
496
497    private void updateNodesWithResolvedArtifacts(List<ArtifactResult> results) {
498        for (ArtifactResult result : results) {
499            Artifact artifact = result.getArtifact();
500            if (artifact != null) {
501                result.getRequest().getDependencyNode().setArtifact(artifact);
502            }
503        }
504    }
505
506    @Override
507    public InstallResult install(RepositorySystemSession session, InstallRequest request) throws InstallationException {
508        validateSession(session);
509        requireNonNull(request, "request cannot be null");
510        repositorySystemValidator.validateInstallRequest(session, request);
511        return installer.install(session, request);
512    }
513
514    @Override
515    public DeployResult deploy(RepositorySystemSession session, DeployRequest request) throws DeploymentException {
516        validateSession(session);
517        requireNonNull(request, "request cannot be null");
518        repositorySystemValidator.validateDeployRequest(session, request);
519        return deployer.deploy(session, request);
520    }
521
522    @Override
523    public LocalRepositoryManager newLocalRepositoryManager(
524            RepositorySystemSession session, LocalRepository localRepository) {
525        requireNonNull(session, "session cannot be null");
526        requireNonNull(localRepository, "localRepository cannot be null");
527        validateSystem();
528        repositorySystemValidator.validateLocalRepositories(session, Collections.singleton(localRepository));
529        return createLocalRepositoryManager(session, localRepository);
530    }
531
532    @Override
533    public LocalRepositoryManager newLocalRepositoryManager(
534            RepositorySystemSession session, LocalRepository... localRepositories) {
535        requireNonNull(session, "session cannot be null");
536        requireNonNull(localRepositories, "localRepositories cannot be null");
537        validateSystem();
538        repositorySystemValidator.validateLocalRepositories(session, Arrays.asList(localRepositories));
539        return createLocalRepositoryManager(session, Arrays.asList(localRepositories));
540    }
541
542    @Override
543    public LocalRepositoryManager newLocalRepositoryManager(
544            RepositorySystemSession session, List<LocalRepository> localRepositories) {
545        requireNonNull(session, "session cannot be null");
546        requireNonNull(localRepositories, "localRepositories cannot be null");
547        validateSystem();
548        repositorySystemValidator.validateLocalRepositories(session, localRepositories);
549        return createLocalRepositoryManager(session, localRepositories);
550    }
551
552    private LocalRepositoryManager createLocalRepositoryManager(
553            RepositorySystemSession session, List<LocalRepository> localRepositories) {
554        if (localRepositories.isEmpty()) {
555            throw new IllegalArgumentException("empty localRepositories");
556        } else if (localRepositories.size() == 1) {
557            return createLocalRepositoryManager(session, localRepositories.get(0));
558        } else {
559            LocalRepositoryManager head = createLocalRepositoryManager(session, localRepositories.get(0));
560            List<LocalRepositoryManager> tail = localRepositories.subList(1, localRepositories.size()).stream()
561                    .map(l -> createLocalRepositoryManager(session, l))
562                    .collect(toList());
563            return new ChainedLocalRepositoryManager(head, tail, session);
564        }
565    }
566
567    private LocalRepositoryManager createLocalRepositoryManager(
568            RepositorySystemSession session, LocalRepository localRepository) {
569        try {
570            return localRepositoryProvider.newLocalRepositoryManager(session, localRepository);
571        } catch (NoLocalRepositoryManagerException e) {
572            throw new IllegalArgumentException(e.getMessage(), e);
573        }
574    }
575
576    @Override
577    public SyncContext newSyncContext(RepositorySystemSession session, boolean shared) {
578        validateSession(session);
579        return syncContextFactory.newInstance(session, shared);
580    }
581
582    @Override
583    public List<RemoteRepository> newResolutionRepositories(
584            RepositorySystemSession session, List<RemoteRepository> repositories) {
585        validateSession(session);
586        validateRepositories(repositories);
587        repositorySystemValidator.validateRemoteRepositories(session, repositories);
588        return remoteRepositoryManager.aggregateRepositories(session, new ArrayList<>(), repositories, true);
589    }
590
591    @Override
592    public RemoteRepository newDeploymentRepository(RepositorySystemSession session, RemoteRepository repository) {
593        validateSession(session);
594        requireNonNull(repository, "repository cannot be null");
595        repositorySystemValidator.validateRemoteRepositories(session, Collections.singletonList(repository));
596        Authentication auth = session.getAuthenticationSelector().getAuthentication(repository);
597        Proxy proxy = session.getProxySelector().getProxy(repository);
598        return new RemoteRepository.Builder(repository)
599                .setAuthentication(auth)
600                .setProxy(proxy)
601                .setIntent(RemoteRepository.Intent.DEPLOYMENT)
602                .build();
603    }
604
605    @Override
606    public void addOnSystemEndedHandler(Runnable handler) {
607        validateSystem();
608        repositorySystemLifecycle.addOnSystemEndedHandler(handler);
609    }
610
611    @Override
612    public RepositorySystemSession.SessionBuilder createSessionBuilder() {
613        validateSystem();
614        return new DefaultSessionBuilder(
615                this, repositorySystemLifecycle, () -> "id-" + sessionIdCounter.incrementAndGet());
616    }
617
618    @Override
619    public void shutdown() {
620        if (shutdown.compareAndSet(false, true)) {
621            repositorySystemLifecycle.systemEnded();
622        }
623    }
624
625    /**
626     * Thread-scoped re-entrancy depth counter. This supplements the {@link RequestTrace}-based
627     * detection for consumers that rebuild the trace chain from a different tracing system
628     * (e.g. Maven 4's {@code RequestTraceHelper} converts between Maven API traces and
629     * resolver traces, losing the {@link #REPOSITORY_SYSTEM_CALL} marker).
630     * <p>
631     * Re-entrancy is inherently per-call-stack (per-thread), so a {@code ThreadLocal} is the
632     * correct semantic. A value &gt; 0 on entry means the current thread is already inside
633     * a {@code RepositorySystem} public method. Unlike a session-scoped counter, this avoids
634     * false positives in parallel builds where multiple threads share a single session.
635     */
636    private static final ThreadLocal<int[]> REENTRY_DEPTH = ThreadLocal.withInitial(() -> new int[] {0});
637
638    /**
639     * Stamps the {@link #REPOSITORY_SYSTEM_CALL} re-entrancy marker into the trace chain
640     * while preserving the original trace tip data. The marker is inserted <em>below</em>
641     * the tip so that code walking the trace and casting {@code getData()} to its expected
642     * type (e.g. {@code org.apache.maven.artifact.Artifact}) still finds the original data
643     * at the tip rather than the anonymous marker object.
644     *
645     * @param currentTrace the current request trace (may be {@code null})
646     * @return a new trace with the marker inserted and original tip data preserved
647     */
648    private static RequestTrace stampReentrancyMarker(RequestTrace currentTrace) {
649        RequestTrace markerTrace = RequestTrace.newChild(currentTrace, REPOSITORY_SYSTEM_CALL);
650        return currentTrace != null ? RequestTrace.newChild(markerTrace, currentTrace.getData()) : markerTrace;
651    }
652
653    /**
654     * Checks whether the given {@link RequestTrace} indicates a re-entrant call by looking
655     * for the {@link #REPOSITORY_SYSTEM_CALL} marker in the trace ancestry.
656     *
657     * @return {@code true} if the marker is found (re-entrant call), {@code false} otherwise
658     */
659    private static boolean isReentrant(RequestTrace trace) {
660        for (RequestTrace t = trace; t != null; t = t.getParent()) {
661            if (t.getData() == REPOSITORY_SYSTEM_CALL) {
662                return true;
663            }
664        }
665        return false;
666    }
667
668    /**
669     * Combined re-entrancy check using both {@link RequestTrace} ancestry and thread-scoped
670     * depth tracking. Either mechanism detecting re-entrancy is sufficient to skip validation.
671     * <p>
672     * The trace-based check is the primary mechanism and works when callers properly propagate
673     * traces. The thread-scoped check is a fallback for callers that rebuild the trace chain
674     * from a different tracing system (e.g. Maven 4's trace conversion loses the resolver's
675     * re-entrancy marker).
676     *
677     * @param trace   the current request trace (may be {@code null})
678     * @param session the current repository system session (unused, kept for signature consistency)
679     * @return {@code true} if this is a re-entrant call, {@code false} if it is the outermost call
680     */
681    private static boolean isReentrant(RequestTrace trace, RepositorySystemSession session) {
682        return isReentrant(trace) || REENTRY_DEPTH.get()[0] > 0;
683    }
684
685    /**
686     * Increments the thread-scoped re-entrancy depth counter. Must be called on every outermost
687     * entry into a public {@code RepositorySystem} method, and the returned {@link Runnable} must
688     * be invoked in a {@code finally} block to decrement the counter on exit.
689     *
690     * @param session the current repository system session (unused, kept for signature consistency)
691     * @return a {@link Runnable} that decrements the depth counter when invoked
692     */
693    private static Runnable enterSessionScope(RepositorySystemSession session) {
694        int[] depth = REENTRY_DEPTH.get();
695        depth[0]++;
696        return () -> depth[0]--;
697    }
698
699    private void validateSession(RepositorySystemSession session) {
700        requireNonNull(session, "repository system session cannot be null");
701        invalidSession(session.getLocalRepositoryManager(), "local repository manager");
702        invalidSession(session.getSystemProperties(), "system properties");
703        invalidSession(session.getUserProperties(), "user properties");
704        invalidSession(session.getConfigProperties(), "config properties");
705        invalidSession(session.getMirrorSelector(), "mirror selector");
706        invalidSession(session.getProxySelector(), "proxy selector");
707        invalidSession(session.getAuthenticationSelector(), "authentication selector");
708        invalidSession(session.getArtifactTypeRegistry(), "artifact type registry");
709        invalidSession(session.getData(), "data");
710        validateSystem();
711    }
712
713    private void validateSystem() {
714        if (shutdown.get()) {
715            throw new IllegalStateException("repository system is already shut down");
716        }
717    }
718
719    private void validateRepositories(List<RemoteRepository> repositories) {
720        requireNonNull(repositories, "repositories cannot be null");
721        for (RemoteRepository repository : repositories) {
722            requireNonNull(repository, "repository cannot be null");
723        }
724    }
725
726    private void invalidSession(Object obj, String name) {
727        requireNonNull(obj, "repository system session's " + name + " cannot be null");
728    }
729}