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.List;
027import java.util.ListIterator;
028import java.util.Map;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.stream.Collectors;
031
032import org.eclipse.aether.ConfigurationProperties;
033import org.eclipse.aether.Keys;
034import org.eclipse.aether.RepositoryCache;
035import org.eclipse.aether.RepositorySystemSession;
036import org.eclipse.aether.impl.RemoteRepositoryManager;
037import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
038import org.eclipse.aether.repository.Authentication;
039import org.eclipse.aether.repository.AuthenticationSelector;
040import org.eclipse.aether.repository.MirrorSelector;
041import org.eclipse.aether.repository.Proxy;
042import org.eclipse.aether.repository.ProxySelector;
043import org.eclipse.aether.repository.RemoteRepository;
044import org.eclipse.aether.repository.RepositoryKeyFunction;
045import org.eclipse.aether.repository.RepositoryPolicy;
046import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
047import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
048import org.eclipse.aether.util.ConfigUtils;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051
052import static java.util.Objects.requireNonNull;
053
054/**
055 */
056@Singleton
057@Named
058public class DefaultRemoteRepositoryManager implements RemoteRepositoryManager {
059
060    private static final String CONFIG_PROPS_PREFIX =
061            ConfigurationProperties.PREFIX_AETHER + "remoteRepositoryManager.";
062
063    /**
064     * Flag indicating whether session authentication (i.e. credentials configured in {@code settings.xml}) may be
065     * applied, matched by plain repository ID, to repositories declared by remote artifact descriptors (POMs) that
066     * are merged into the effective repository list during dependency collection. When disabled (the default),
067     * session authentication is only applied to such a repository when an operator-defined mirror has been selected
068     * for it; if credentials would have matched a descriptor-declared repository, a warning naming the repository ID
069     * and URL is logged instead. Repositories supplied by the build itself (e.g. aggregated via
070     * {@code RepositorySystem#newResolutionRepositories}) are unaffected and keep receiving matching credentials.
071     * Enabling this restores the legacy behavior of applying matching session authentication to descriptor
072     * declared repositories regardless of their provenance.
073     *
074     * @since 2.0.23
075     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
076     * @configurationType {@link java.lang.Boolean}
077     * @configurationDefaultValue {@link #DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES}
078     */
079    public static final String CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES =
080            CONFIG_PROPS_PREFIX + "authToDescriptorRepositories";
081
082    public static final boolean DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES = false;
083
084    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRemoteRepositoryManager.class);
085
086    private final UpdatePolicyAnalyzer updatePolicyAnalyzer;
087
088    private final ChecksumPolicyProvider checksumPolicyProvider;
089
090    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
091
092    @Inject
093    public DefaultRemoteRepositoryManager(
094            UpdatePolicyAnalyzer updatePolicyAnalyzer,
095            ChecksumPolicyProvider checksumPolicyProvider,
096            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory) {
097        this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null");
098        this.checksumPolicyProvider = requireNonNull(checksumPolicyProvider, "checksum policy provider cannot be null");
099        this.repositoryKeyFunctionFactory =
100                requireNonNull(repositoryKeyFunctionFactory, "repository key function factory cannot be null");
101    }
102
103    @Override
104    public List<RemoteRepository> aggregateRepositories(
105            RepositorySystemSession session,
106            List<RemoteRepository> dominantRepositories,
107            List<RemoteRepository> recessiveRepositories,
108            boolean recessiveIsRaw) {
109        return aggregateRepositories(session, dominantRepositories, recessiveRepositories, recessiveIsRaw, false);
110    }
111
112    @Override
113    public List<RemoteRepository> aggregateRepositories(
114            RepositorySystemSession session,
115            List<RemoteRepository> dominantRepositories,
116            List<RemoteRepository> recessiveRepositories,
117            boolean recessiveIsRaw,
118            boolean recessiveIsFromDescriptor) {
119        requireNonNull(session, "session cannot be null");
120        requireNonNull(dominantRepositories, "dominantRepositories cannot be null");
121        requireNonNull(recessiveRepositories, "recessiveRepositories cannot be null");
122        if (recessiveRepositories.isEmpty()) {
123            return dominantRepositories;
124        }
125
126        RepositoryKeyFunction repositoryKeyFunction = repositoryKeyFunctionFactory.systemRepositoryKeyFunction(session);
127        MirrorSelector mirrorSelector = session.getMirrorSelector();
128        AuthenticationSelector authSelector = session.getAuthenticationSelector();
129        ProxySelector proxySelector = session.getProxySelector();
130
131        boolean authToDescriptorRepositories = ConfigUtils.getBoolean(
132                session, DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES, CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES);
133
134        List<RemoteRepository> result = new ArrayList<>(dominantRepositories);
135
136        next:
137        for (RemoteRepository recessiveRepository : recessiveRepositories) {
138            RemoteRepository repository = recessiveRepository;
139            boolean mirrored = false;
140
141            if (recessiveIsRaw
142                    && (recessiveIsFromDescriptor
143                            || recessiveRepository.getMirroredRepositories().isEmpty())) {
144                RemoteRepository mirrorRepository = mirrorSelector.getMirror(recessiveRepository);
145
146                if (mirrorRepository != null) {
147                    logMirror(session, recessiveRepository, mirrorRepository);
148                    repository = mirrorRepository;
149                    mirrored = true;
150                }
151            }
152
153            String key = repositoryKeyFunction.apply(repository, null);
154
155            for (ListIterator<RemoteRepository> it = result.listIterator(); it.hasNext(); ) {
156                RemoteRepository dominantRepository = it.next();
157
158                if (key.equals(repositoryKeyFunction.apply(dominantRepository, null))) {
159                    if (!dominantRepository.getMirroredRepositories().isEmpty()
160                            && !repository.getMirroredRepositories().isEmpty()) {
161                        RemoteRepository mergedRepository = mergeMirrors(
162                                session, repositoryKeyFunction, dominantRepository, repository, recessiveIsRaw);
163                        if (mergedRepository != dominantRepository) {
164                            it.set(mergedRepository);
165                        }
166                    }
167
168                    continue next;
169                }
170            }
171
172            if (recessiveIsRaw) {
173                RemoteRepository.Builder builder = null;
174                Authentication auth = authSelector.getAuthentication(repository);
175                if (auth != null) {
176                    if (!recessiveIsFromDescriptor || mirrored || authToDescriptorRepositories) {
177                        builder = new RemoteRepository.Builder(repository);
178                        builder.setAuthentication(auth);
179                    } else if (auth != repository.getAuthentication()) {
180                        logWarnOnce(
181                                session,
182                                "Not applying session authentication to repository {} ({}) declared by a remote"
183                                        + " artifact descriptor; set {}=true to restore the legacy behavior of"
184                                        + " matching credentials to such repositories by repository ID",
185                                repository.getId(),
186                                repository.getUrl(),
187                                CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES);
188                    }
189                }
190                Proxy proxy = proxySelector.getProxy(repository);
191                if (proxy != null) {
192                    if (builder == null) {
193                        builder = new RemoteRepository.Builder(repository);
194                    }
195                    builder.setProxy(proxy);
196                }
197                if (builder != null) {
198                    repository = builder.build();
199                }
200            }
201
202            result.add(repository);
203        }
204
205        return result.stream()
206                .map(r -> new RemoteRepository.Builder(r)
207                        .setIntent(RemoteRepository.Intent.RESOLUTION)
208                        .build())
209                .collect(Collectors.toList());
210    }
211
212    private void logMirror(RepositorySystemSession session, RemoteRepository original, RemoteRepository mirror) {
213        if (!LOGGER.isDebugEnabled()) {
214            return;
215        }
216        RepositoryCache cache = session.getCache();
217        if (cache != null) {
218            Object key = Keys.of(mirror.getId(), mirror.getUrl(), original.getId(), original.getUrl());
219            if (cache.get(session, key) != null) {
220                return;
221            }
222            cache.put(session, key, Boolean.TRUE);
223        }
224        LOGGER.debug(
225                "Using mirror {} ({}) for {} ({}).",
226                mirror.getId(),
227                mirror.getUrl(),
228                original.getId(),
229                original.getUrl());
230    }
231
232    /**
233     * Flag indicating whether a repository declared by a remote artifact descriptor (POM) may weaken the checksum
234     * policy of the operator-defined mirror it is merged into. When disabled (the default), the effective checksum
235     * policy of a mirror never becomes weaker than what the mirror itself declares for the same nature: a recessive
236     * raw repository may still enable a nature or influence update policies, but a weaker checksum policy (e.g.
237     * {@code <checksumPolicy>ignore</checksumPolicy>} in a transitive POM) is not honored and a warning is logged
238     * instead. Enabling this restores the legacy weakest-wins merge, which let any POM in the dependency graph
239     * degrade or switch off checksum verification for downloads routed through the mirror.
240     *
241     * @since 2.0.23
242     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
243     * @configurationType {@link java.lang.Boolean}
244     * @configurationDefaultValue {@link #DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE}
245     */
246    public static final String CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE =
247            "aether.remoteRepositoryManager.rawRepositoryChecksumPolicyDowngrade";
248
249    public static final boolean DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE = false;
250
251    private static RepositoryPolicy clampChecksumPolicy(
252            RepositorySystemSession session, RepositoryPolicy merged, RepositoryPolicy floor, RemoteRepository rec) {
253        if (merged == null || floor == null) {
254            return merged;
255        }
256        if (checksumPolicyRank(merged.getChecksumPolicy()) < checksumPolicyRank(floor.getChecksumPolicy())) {
257            logWarnOnce(
258                    session,
259                    "Ignoring checksum policy '{}' contributed by repository {} ({}) declared by a remote artifact"
260                            + " descriptor: it would downgrade the checksum policy '{}' of the mirror serving it;"
261                            + " set {}=true to restore the legacy weakest-wins merge",
262                    merged.getChecksumPolicy(),
263                    rec.getId(),
264                    rec.getUrl(),
265                    floor.getChecksumPolicy(),
266                    CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE);
267            return new RepositoryPolicy(
268                    merged.isEnabled(),
269                    merged.getArtifactUpdatePolicy(),
270                    merged.getMetadataUpdatePolicy(),
271                    floor.getChecksumPolicy());
272        }
273        return merged;
274    }
275
276    private static int checksumPolicyRank(String policy) {
277        if (policy == null) {
278            return -1;
279        }
280        switch (policy) {
281            case RepositoryPolicy.CHECKSUM_POLICY_FAIL:
282                return 2;
283            case RepositoryPolicy.CHECKSUM_POLICY_WARN:
284                return 1;
285            case RepositoryPolicy.CHECKSUM_POLICY_IGNORE:
286                return 0;
287            default:
288                // unknown (potentially attacker-supplied) values rank below any known policy, so they can never
289                // displace an operator-configured one
290                return -1;
291        }
292    }
293
294    private RemoteRepository mergeMirrors(
295            RepositorySystemSession session,
296            RepositoryKeyFunction repositoryKeyFunction,
297            RemoteRepository dominant,
298            RemoteRepository recessive,
299            boolean recessiveIsRaw) {
300        boolean rawChecksumPolicyDowngrade = org.eclipse.aether.util.ConfigUtils.getBoolean(
301                session, DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE, CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE);
302        RemoteRepository.Builder merged = null;
303        RepositoryPolicy releases = null, snapshots = null;
304
305        next:
306        for (RemoteRepository rec : recessive.getMirroredRepositories()) {
307            String recKey = repositoryKeyFunction.apply(rec, null);
308
309            for (RemoteRepository dom : dominant.getMirroredRepositories()) {
310                if (recKey.equals(repositoryKeyFunction.apply(dom, null))) {
311                    continue next;
312                }
313            }
314
315            if (merged == null) {
316                merged = new RemoteRepository.Builder(dominant);
317                releases = dominant.getPolicy(false);
318                snapshots = dominant.getPolicy(true);
319            }
320
321            releases = merge(session, releases, rec.getPolicy(false), false);
322            snapshots = merge(session, snapshots, rec.getPolicy(true), false);
323
324            if (recessiveIsRaw && !rawChecksumPolicyDowngrade) {
325                // "recessive" originates from a remote artifact descriptor (POM): remotely supplied input must
326                // never weaken the checksum policy the operator configured on the mirror itself
327                releases = clampChecksumPolicy(session, releases, dominant.getPolicy(false), rec);
328                snapshots = clampChecksumPolicy(session, snapshots, dominant.getPolicy(true), rec);
329            }
330
331            merged.addMirroredRepository(rec);
332        }
333
334        if (merged == null) {
335            return dominant;
336        }
337        return merged.setReleasePolicy(releases).setSnapshotPolicy(snapshots).build();
338    }
339
340    /**
341     * Flag indicating whether the merge of a repository's release and snapshot policies (used when a single
342     * effective policy has to serve both natures, most notably for metadata of nature RELEASE_OR_SNAPSHOT such as
343     * {@code maven-metadata.xml} version lists) may pick the <em>weaker</em> of the two checksum policies, which was
344     * the legacy behavior. When disabled (the default), the stronger of the two checksum policies wins, so enabling
345     * snapshots with a lenient checksum policy no longer silently downgrades checksum enforcement below what the
346     * operator configured for releases (or vice versa). An explicit checksum policy set on the session (e.g. via
347     * {@code --strict-checksums}) takes precedence over either behavior, as before.
348     *
349     * @since 2.0.23
350     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
351     * @configurationType {@link java.lang.Boolean}
352     * @configurationDefaultValue {@link #DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY}
353     */
354    public static final String CONFIG_PROP_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY =
355            "aether.remoteRepositoryManager.natureMergeWeakestChecksumPolicy";
356
357    public static final boolean DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY = false;
358
359    /**
360     * Flag indicating whether the merge of a repository's release and snapshot update policies (used when a single
361     * effective policy has to serve both natures, most notably for metadata of nature RELEASE_OR_SNAPSHOT such as
362     * {@code maven-metadata.xml} version lists) may pick the policy with the <em>shorter</em> update interval of the
363     * two, which was the legacy behavior. When disabled (the default), the policy with the longer update interval
364     * wins, so enabling snapshots on a repository configured with {@code updatePolicy=never} for releases no longer
365     * silently forces metadata updates that the operator configured to never happen (or vice versa). An explicit
366     * update policy set on the session takes precedence over either behavior, as before.
367     *
368     * @since 2.0.25
369     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
370     * @configurationType {@link java.lang.Boolean}
371     * @configurationDefaultValue {@link #DEFAULT_NATURE_MERGE_WEAKEST_UPDATE_POLICY}
372     */
373    public static final String CONFIG_PROP_NATURE_MERGE_WEAKEST_UPDATE_POLICY =
374            "aether.remoteRepositoryManager.natureMergeWeakestUpdatePolicy";
375
376    public static final boolean DEFAULT_NATURE_MERGE_WEAKEST_UPDATE_POLICY = false;
377
378    @Override
379    public RepositoryPolicy getPolicy(
380            RepositorySystemSession session, RemoteRepository repository, boolean releases, boolean snapshots) {
381        requireNonNull(session, "session cannot be null");
382        requireNonNull(repository, "repository cannot be null");
383        RepositoryPolicy policy1 = releases ? repository.getPolicy(false) : null;
384        RepositoryPolicy policy2 = snapshots ? repository.getPolicy(true) : null;
385        return merge(session, policy1, policy2, true);
386    }
387
388    private RepositoryPolicy merge(
389            RepositorySystemSession session, RepositoryPolicy policy1, RepositoryPolicy policy2, boolean globalPolicy) {
390        RepositoryPolicy policy;
391
392        if (policy2 == null) {
393            if (globalPolicy) {
394                policy = merge(
395                        policy1,
396                        session.getArtifactUpdatePolicy(),
397                        session.getMetadataUpdatePolicy(),
398                        session.getChecksumPolicy());
399            } else {
400                policy = policy1;
401            }
402        } else if (policy1 == null) {
403            if (globalPolicy) {
404                policy = merge(
405                        policy2,
406                        session.getArtifactUpdatePolicy(),
407                        session.getMetadataUpdatePolicy(),
408                        session.getChecksumPolicy());
409            } else {
410                policy = policy2;
411            }
412        } else if (!policy2.isEnabled()) {
413            if (globalPolicy) {
414                policy = merge(
415                        policy1,
416                        session.getArtifactUpdatePolicy(),
417                        session.getMetadataUpdatePolicy(),
418                        session.getChecksumPolicy());
419            } else {
420                policy = policy1;
421            }
422        } else if (!policy1.isEnabled()) {
423            if (globalPolicy) {
424                policy = merge(
425                        policy2,
426                        session.getArtifactUpdatePolicy(),
427                        session.getMetadataUpdatePolicy(),
428                        session.getChecksumPolicy());
429            } else {
430                policy = policy2;
431            }
432        } else {
433            String checksums = session.getChecksumPolicy();
434            //noinspection StatementWithEmptyBody
435            if (globalPolicy && checksums != null && !checksums.isEmpty()) {
436                // use global override
437            } else if (globalPolicy
438                    && !org.eclipse.aether.util.ConfigUtils.getBoolean(
439                            session,
440                            DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY,
441                            CONFIG_PROP_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY)) {
442                // merging the release and snapshot policies of a single repository into one effective policy
443                // (e.g. for metadata of nature RELEASE_OR_SNAPSHOT): the result must not be weaker than what the
444                // operator configured for either nature, so the stronger checksum policy wins
445                checksums = strongerChecksumPolicy(policy1.getChecksumPolicy(), policy2.getChecksumPolicy());
446            } else {
447                checksums = checksumPolicyProvider.getEffectiveChecksumPolicy(
448                        session, policy1.getChecksumPolicy(), policy2.getChecksumPolicy());
449            }
450
451            boolean mergeWeakestUpdatePolicy = ConfigUtils.getBoolean(
452                    session,
453                    DEFAULT_NATURE_MERGE_WEAKEST_UPDATE_POLICY,
454                    CONFIG_PROP_NATURE_MERGE_WEAKEST_UPDATE_POLICY);
455
456            String artifactUpdates = session.getArtifactUpdatePolicy();
457            //noinspection StatementWithEmptyBody
458            if (globalPolicy && artifactUpdates != null && !artifactUpdates.isEmpty()) {
459                // use global override
460            } else if (globalPolicy && !mergeWeakestUpdatePolicy) {
461                // merging the release and snapshot policies of a single repository into one effective policy
462                // (e.g. for metadata of nature RELEASE_OR_SNAPSHOT): the result must not be more eager than what
463                // the operator configured for either nature, so the policy with the longer update interval wins
464                artifactUpdates = updatePolicyAnalyzer.getStrongerUpdatePolicy(
465                        session, policy1.getArtifactUpdatePolicy(), policy2.getArtifactUpdatePolicy());
466            } else {
467                artifactUpdates = updatePolicyAnalyzer.getEffectiveUpdatePolicy(
468                        session, policy1.getArtifactUpdatePolicy(), policy2.getArtifactUpdatePolicy());
469            }
470            String metadataUpdates = session.getMetadataUpdatePolicy();
471            if (globalPolicy && metadataUpdates != null && !metadataUpdates.isEmpty()) {
472                // use global override
473            } else if (globalPolicy && !mergeWeakestUpdatePolicy) {
474                // same reasoning as above, for metadata update policy; this is the composite RELEASE_OR_SNAPSHOT
475                // case that version-range and LATEST/RELEASE metadata requests go through
476                metadataUpdates = updatePolicyAnalyzer.getStrongerUpdatePolicy(
477                        session, policy1.getMetadataUpdatePolicy(), policy2.getMetadataUpdatePolicy());
478            } else {
479                metadataUpdates = updatePolicyAnalyzer.getEffectiveUpdatePolicy(
480                        session, policy1.getMetadataUpdatePolicy(), policy2.getMetadataUpdatePolicy());
481            }
482
483            policy = new RepositoryPolicy(true, artifactUpdates, metadataUpdates, checksums);
484        }
485
486        return policy;
487    }
488
489    private RepositoryPolicy merge(
490            RepositoryPolicy policy, String artifactUpdates, String metadataUpdates, String checksums) {
491        if (policy != null) {
492            if (artifactUpdates == null || artifactUpdates.isEmpty()) {
493                artifactUpdates = policy.getArtifactUpdatePolicy();
494            }
495            if (metadataUpdates == null || metadataUpdates.isEmpty()) {
496                metadataUpdates = policy.getMetadataUpdatePolicy();
497            }
498            if (checksums == null || checksums.isEmpty()) {
499                checksums = policy.getChecksumPolicy();
500            }
501            if (!policy.getArtifactUpdatePolicy().equals(artifactUpdates)
502                    || !policy.getMetadataUpdatePolicy().equals(metadataUpdates)
503                    || !policy.getChecksumPolicy().equals(checksums)) {
504                policy = new RepositoryPolicy(policy.isEnabled(), artifactUpdates, metadataUpdates, checksums);
505            }
506        }
507        return policy;
508    }
509
510    private static String strongerChecksumPolicy(String policy1, String policy2) {
511        if (checksumPolicyStrength(policy2) > checksumPolicyStrength(policy1)) {
512            return policy2;
513        }
514        return policy1;
515    }
516
517    private static int checksumPolicyStrength(String policy) {
518        if (policy == null) {
519            return -1;
520        }
521        switch (policy) {
522            case RepositoryPolicy.CHECKSUM_POLICY_FAIL:
523                return 2;
524            case RepositoryPolicy.CHECKSUM_POLICY_WARN:
525                return 1;
526            case RepositoryPolicy.CHECKSUM_POLICY_IGNORE:
527                return 0;
528            default:
529                // unknown values never win a strength comparison; they are rejected downstream when a
530                // ChecksumPolicy instance is created for them
531                return -1;
532        }
533    }
534
535    @SuppressWarnings("unchecked")
536    private static void logWarnOnce(RepositorySystemSession session, String message, Object... args) {
537        if (!LOGGER.isWarnEnabled()) {
538            return;
539        }
540        Object[] keys = new Object[args.length + 1];
541        keys[0] = message;
542        System.arraycopy(args, 0, keys, 1, args.length);
543        Object key = Keys.of(keys);
544        ((Map<Object, Boolean>) session.getData()
545                        .computeIfAbsent(
546                                Keys.of(DefaultRemoteRepositoryManager.class, "logWarnOnce"), ConcurrentHashMap::new))
547                .computeIfAbsent(key, k -> {
548                    LOGGER.warn(message, args);
549                    return true;
550                });
551    }
552}