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}