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;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Objects;
32  import java.util.concurrent.atomic.AtomicBoolean;
33  import java.util.concurrent.atomic.AtomicInteger;
34  import java.util.function.Consumer;
35  import java.util.stream.Collectors;
36  
37  import org.eclipse.aether.ConfigurationProperties;
38  import org.eclipse.aether.RepositorySystem;
39  import org.eclipse.aether.RepositorySystemSession;
40  import org.eclipse.aether.RequestTrace;
41  import org.eclipse.aether.SyncContext;
42  import org.eclipse.aether.artifact.Artifact;
43  import org.eclipse.aether.collection.CollectRequest;
44  import org.eclipse.aether.collection.CollectResult;
45  import org.eclipse.aether.collection.DependencyCollectionException;
46  import org.eclipse.aether.deployment.DeployRequest;
47  import org.eclipse.aether.deployment.DeployResult;
48  import org.eclipse.aether.deployment.DeploymentException;
49  import org.eclipse.aether.graph.DependencyFilter;
50  import org.eclipse.aether.graph.DependencyNode;
51  import org.eclipse.aether.graph.DependencyVisitor;
52  import org.eclipse.aether.impl.ArtifactDescriptorReader;
53  import org.eclipse.aether.impl.ArtifactResolver;
54  import org.eclipse.aether.impl.DependencyCollector;
55  import org.eclipse.aether.impl.Deployer;
56  import org.eclipse.aether.impl.Installer;
57  import org.eclipse.aether.impl.LocalRepositoryProvider;
58  import org.eclipse.aether.impl.MetadataResolver;
59  import org.eclipse.aether.impl.RemoteRepositoryManager;
60  import org.eclipse.aether.impl.RepositorySystemLifecycle;
61  import org.eclipse.aether.impl.RepositorySystemValidator;
62  import org.eclipse.aether.impl.VersionRangeResolver;
63  import org.eclipse.aether.impl.VersionResolver;
64  import org.eclipse.aether.installation.InstallRequest;
65  import org.eclipse.aether.installation.InstallResult;
66  import org.eclipse.aether.installation.InstallationException;
67  import org.eclipse.aether.internal.impl.session.DefaultSessionBuilder;
68  import org.eclipse.aether.repository.Authentication;
69  import org.eclipse.aether.repository.LocalRepository;
70  import org.eclipse.aether.repository.LocalRepositoryManager;
71  import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
72  import org.eclipse.aether.repository.Proxy;
73  import org.eclipse.aether.repository.RemoteRepository;
74  import org.eclipse.aether.resolution.ArtifactDescriptorException;
75  import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
76  import org.eclipse.aether.resolution.ArtifactDescriptorResult;
77  import org.eclipse.aether.resolution.ArtifactRequest;
78  import org.eclipse.aether.resolution.ArtifactResolutionException;
79  import org.eclipse.aether.resolution.ArtifactResult;
80  import org.eclipse.aether.resolution.DependencyRequest;
81  import org.eclipse.aether.resolution.DependencyResolutionException;
82  import org.eclipse.aether.resolution.DependencyResult;
83  import org.eclipse.aether.resolution.MetadataRequest;
84  import org.eclipse.aether.resolution.MetadataResult;
85  import org.eclipse.aether.resolution.VersionRangeRequest;
86  import org.eclipse.aether.resolution.VersionRangeResolutionException;
87  import org.eclipse.aether.resolution.VersionRangeResult;
88  import org.eclipse.aether.resolution.VersionRequest;
89  import org.eclipse.aether.resolution.VersionResolutionException;
90  import org.eclipse.aether.resolution.VersionResult;
91  import org.eclipse.aether.spi.artifact.decorator.ArtifactDecorator;
92  import org.eclipse.aether.spi.artifact.decorator.ArtifactDecoratorFactory;
93  import org.eclipse.aether.spi.synccontext.SyncContextFactory;
94  import org.eclipse.aether.util.ConfigUtils;
95  import org.eclipse.aether.util.graph.visitor.LevelOrderDependencyNodeConsumerVisitor;
96  import org.eclipse.aether.util.graph.visitor.PostorderDependencyNodeConsumerVisitor;
97  import org.eclipse.aether.util.graph.visitor.PreorderDependencyNodeConsumerVisitor;
98  import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager;
99  
100 import static java.util.Objects.requireNonNull;
101 import static java.util.stream.Collectors.toList;
102 
103 /**
104  *
105  */
106 @Singleton
107 @Named
108 public 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 }