001package org.eclipse.aether.internal.impl; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.File; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.Map; 026import static java.util.Objects.requireNonNull; 027import java.util.Properties; 028import java.util.Set; 029import java.util.TreeSet; 030import java.util.concurrent.ConcurrentHashMap; 031 032import javax.inject.Inject; 033import javax.inject.Named; 034import javax.inject.Singleton; 035 036import org.eclipse.aether.RepositorySystemSession; 037import org.eclipse.aether.SessionData; 038import org.eclipse.aether.artifact.Artifact; 039import org.eclipse.aether.impl.UpdateCheck; 040import org.eclipse.aether.impl.UpdateCheckManager; 041import org.eclipse.aether.impl.UpdatePolicyAnalyzer; 042import org.eclipse.aether.metadata.Metadata; 043import org.eclipse.aether.repository.AuthenticationDigest; 044import org.eclipse.aether.repository.Proxy; 045import org.eclipse.aether.repository.RemoteRepository; 046import org.eclipse.aether.resolution.ResolutionErrorPolicy; 047import org.eclipse.aether.spi.locator.Service; 048import org.eclipse.aether.spi.locator.ServiceLocator; 049import org.eclipse.aether.transfer.ArtifactNotFoundException; 050import org.eclipse.aether.transfer.ArtifactTransferException; 051import org.eclipse.aether.transfer.MetadataNotFoundException; 052import org.eclipse.aether.transfer.MetadataTransferException; 053import org.eclipse.aether.util.ConfigUtils; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056 057/** 058 */ 059@Singleton 060@Named 061public class DefaultUpdateCheckManager 062 implements UpdateCheckManager, Service 063{ 064 065 private static final Logger LOGGER = LoggerFactory.getLogger( DefaultUpdatePolicyAnalyzer.class ); 066 067 private TrackingFileManager trackingFileManager; 068 069 private UpdatePolicyAnalyzer updatePolicyAnalyzer; 070 071 private static final String UPDATED_KEY_SUFFIX = ".lastUpdated"; 072 073 private static final String ERROR_KEY_SUFFIX = ".error"; 074 075 private static final String NOT_FOUND = ""; 076 077 static final Object SESSION_CHECKS = new Object() 078 { 079 @Override 080 public String toString() 081 { 082 return "updateCheckManager.checks"; 083 } 084 }; 085 086 static final String CONFIG_PROP_SESSION_STATE = "aether.updateCheckManager.sessionState"; 087 088 private static final int STATE_ENABLED = 0; 089 090 private static final int STATE_BYPASS = 1; 091 092 private static final int STATE_DISABLED = 2; 093 094 public DefaultUpdateCheckManager() 095 { 096 // default ctor for ServiceLocator 097 } 098 099 @Inject 100 DefaultUpdateCheckManager( TrackingFileManager trackingFileManager, UpdatePolicyAnalyzer updatePolicyAnalyzer ) 101 { 102 setTrackingFileManager( trackingFileManager ); 103 setUpdatePolicyAnalyzer( updatePolicyAnalyzer ); 104 } 105 106 public void initService( ServiceLocator locator ) 107 { 108 setTrackingFileManager( locator.getService( TrackingFileManager.class ) ); 109 setUpdatePolicyAnalyzer( locator.getService( UpdatePolicyAnalyzer.class ) ); 110 } 111 112 public DefaultUpdateCheckManager setTrackingFileManager( TrackingFileManager trackingFileManager ) 113 { 114 this.trackingFileManager = requireNonNull( trackingFileManager ); 115 return this; 116 } 117 118 public DefaultUpdateCheckManager setUpdatePolicyAnalyzer( UpdatePolicyAnalyzer updatePolicyAnalyzer ) 119 { 120 this.updatePolicyAnalyzer = requireNonNull( updatePolicyAnalyzer, "update policy analyzer cannot be null" ); 121 return this; 122 } 123 124 public void checkArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check ) 125 { 126 requireNonNull( session, "session cannot be null" ); 127 requireNonNull( check, "check cannot be null" ); 128 if ( check.getLocalLastUpdated() != 0 129 && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) ) 130 { 131 LOGGER.debug( "Skipped remote request for {}, locally installed artifact up-to-date", check.getItem() ); 132 133 check.setRequired( false ); 134 return; 135 } 136 137 Artifact artifact = check.getItem(); 138 RemoteRepository repository = check.getRepository(); 139 140 File artifactFile = requireNonNull( check.getFile(), String.format( "The artifact '%s' has no file attached", 141 artifact ) ); 142 143 boolean fileExists = check.isFileValid() && artifactFile.exists(); 144 145 File touchFile = getArtifactTouchFile( artifactFile ); 146 Properties props = read( touchFile ); 147 148 String updateKey = getUpdateKey( session, artifactFile, repository ); 149 String dataKey = getDataKey( repository ); 150 151 String error = getError( props, dataKey ); 152 153 long lastUpdated; 154 if ( error == null ) 155 { 156 if ( fileExists ) 157 { 158 // last update was successful 159 lastUpdated = artifactFile.lastModified(); 160 } 161 else 162 { 163 // this is the first attempt ever 164 lastUpdated = 0L; 165 } 166 } 167 else if ( error.isEmpty() ) 168 { 169 // artifact did not exist 170 lastUpdated = getLastUpdated( props, dataKey ); 171 } 172 else 173 { 174 // artifact could not be transferred 175 String transferKey = getTransferKey( session, repository ); 176 lastUpdated = getLastUpdated( props, transferKey ); 177 } 178 179 if ( lastUpdated == 0L ) 180 { 181 check.setRequired( true ); 182 } 183 else if ( isAlreadyUpdated( session, updateKey ) ) 184 { 185 LOGGER.debug( "Skipped remote request for {}, already updated during this session", check.getItem() ); 186 187 check.setRequired( false ); 188 if ( error != null ) 189 { 190 check.setException( newException( error, artifact, repository ) ); 191 } 192 } 193 else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) ) 194 { 195 check.setRequired( true ); 196 } 197 else if ( fileExists ) 198 { 199 LOGGER.debug( "Skipped remote request for {}, locally cached artifact up-to-date", check.getItem() ); 200 201 check.setRequired( false ); 202 } 203 else 204 { 205 int errorPolicy = Utils.getPolicy( session, artifact, repository ); 206 int cacheFlag = getCacheFlag( error ); 207 if ( ( errorPolicy & cacheFlag ) != 0 ) 208 { 209 check.setRequired( false ); 210 check.setException( newException( error, artifact, repository ) ); 211 } 212 else 213 { 214 check.setRequired( true ); 215 } 216 } 217 } 218 219 private static int getCacheFlag( String error ) 220 { 221 if ( error == null || error.isEmpty() ) 222 { 223 return ResolutionErrorPolicy.CACHE_NOT_FOUND; 224 } 225 else 226 { 227 return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR; 228 } 229 } 230 231 private ArtifactTransferException newException( String error, Artifact artifact, RemoteRepository repository ) 232 { 233 if ( error == null || error.isEmpty() ) 234 { 235 return new ArtifactNotFoundException( artifact, repository, artifact 236 + " was not found in " + repository.getUrl() + " during a previous attempt. This failure was" 237 + " cached in the local repository and" 238 + " resolution is not reattempted until the update interval of " + repository.getId() 239 + " has elapsed or updates are forced", true ); 240 } 241 else 242 { 243 return new ArtifactTransferException( artifact, repository, artifact + " failed to transfer from " 244 + repository.getUrl() + " during a previous attempt. This failure" 245 + " was cached in the local repository and" 246 + " resolution is not reattempted until the update interval of " + repository.getId() 247 + " has elapsed or updates are forced. Original error: " + error, true ); 248 } 249 } 250 251 public void checkMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check ) 252 { 253 requireNonNull( session, "session cannot be null" ); 254 requireNonNull( check, "check cannot be null" ); 255 if ( check.getLocalLastUpdated() != 0 256 && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) ) 257 { 258 LOGGER.debug( "Skipped remote request for {} locally installed metadata up-to-date", check.getItem() ); 259 260 check.setRequired( false ); 261 return; 262 } 263 264 Metadata metadata = check.getItem(); 265 RemoteRepository repository = check.getRepository(); 266 267 File metadataFile = requireNonNull( check.getFile(), String.format( "The metadata '%s' has no file attached", 268 metadata ) ); 269 270 boolean fileExists = check.isFileValid() && metadataFile.exists(); 271 272 File touchFile = getMetadataTouchFile( metadataFile ); 273 Properties props = read( touchFile ); 274 275 String updateKey = getUpdateKey( session, metadataFile, repository ); 276 String dataKey = getDataKey( metadataFile ); 277 278 String error = getError( props, dataKey ); 279 280 long lastUpdated; 281 if ( error == null ) 282 { 283 if ( fileExists ) 284 { 285 // last update was successful 286 lastUpdated = getLastUpdated( props, dataKey ); 287 } 288 else 289 { 290 // this is the first attempt ever 291 lastUpdated = 0L; 292 } 293 } 294 else if ( error.isEmpty() ) 295 { 296 // metadata did not exist 297 lastUpdated = getLastUpdated( props, dataKey ); 298 } 299 else 300 { 301 // metadata could not be transferred 302 String transferKey = getTransferKey( session, metadataFile, repository ); 303 lastUpdated = getLastUpdated( props, transferKey ); 304 } 305 306 if ( lastUpdated == 0L ) 307 { 308 check.setRequired( true ); 309 } 310 else if ( isAlreadyUpdated( session, updateKey ) ) 311 { 312 LOGGER.debug( "Skipped remote request for {}, already updated during this session", check.getItem() ); 313 314 check.setRequired( false ); 315 if ( error != null ) 316 { 317 check.setException( newException( error, metadata, repository ) ); 318 } 319 } 320 else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) ) 321 { 322 check.setRequired( true ); 323 } 324 else if ( fileExists ) 325 { 326 LOGGER.debug( "Skipped remote request for {}, locally cached metadata up-to-date", check.getItem() ); 327 328 check.setRequired( false ); 329 } 330 else 331 { 332 int errorPolicy = Utils.getPolicy( session, metadata, repository ); 333 int cacheFlag = getCacheFlag( error ); 334 if ( ( errorPolicy & cacheFlag ) != 0 ) 335 { 336 check.setRequired( false ); 337 check.setException( newException( error, metadata, repository ) ); 338 } 339 else 340 { 341 check.setRequired( true ); 342 } 343 } 344 } 345 346 private MetadataTransferException newException( String error, Metadata metadata, RemoteRepository repository ) 347 { 348 if ( error == null || error.isEmpty() ) 349 { 350 return new MetadataNotFoundException( metadata, repository, metadata + " was not found in " 351 + repository.getUrl() + " during a previous attempt." 352 + " This failure was cached in the local repository and" 353 + " resolution is not be reattempted until the update interval of " + repository.getId() 354 + " has elapsed or updates are forced", true ); 355 } 356 else 357 { 358 return new MetadataTransferException( metadata, repository, metadata + " failed to transfer from " 359 + repository.getUrl() + " during a previous attempt." 360 + " This failure was cached in the local repository and" 361 + " resolution will not be reattempted until the update interval of " + repository.getId() 362 + " has elapsed or updates are forced. Original error: " + error, true ); 363 } 364 } 365 366 private long getLastUpdated( Properties props, String key ) 367 { 368 String value = props.getProperty( key + UPDATED_KEY_SUFFIX, "" ); 369 try 370 { 371 return ( value.length() > 0 ) ? Long.parseLong( value ) : 1; 372 } 373 catch ( NumberFormatException e ) 374 { 375 LOGGER.debug( "Cannot parse last updated date {}, ignoring it", value, e ); 376 return 1; 377 } 378 } 379 380 private String getError( Properties props, String key ) 381 { 382 return props.getProperty( key + ERROR_KEY_SUFFIX ); 383 } 384 385 private File getArtifactTouchFile( File artifactFile ) 386 { 387 return new File( artifactFile.getPath() + UPDATED_KEY_SUFFIX ); 388 } 389 390 private File getMetadataTouchFile( File metadataFile ) 391 { 392 return new File( metadataFile.getParent(), "resolver-status.properties" ); 393 } 394 395 private String getDataKey( RemoteRepository repository ) 396 { 397 Set<String> mirroredUrls = Collections.emptySet(); 398 if ( repository.isRepositoryManager() ) 399 { 400 mirroredUrls = new TreeSet<>(); 401 for ( RemoteRepository mirroredRepository : repository.getMirroredRepositories() ) 402 { 403 mirroredUrls.add( normalizeRepoUrl( mirroredRepository.getUrl() ) ); 404 } 405 } 406 407 StringBuilder buffer = new StringBuilder( 1024 ); 408 409 buffer.append( normalizeRepoUrl( repository.getUrl() ) ); 410 for ( String mirroredUrl : mirroredUrls ) 411 { 412 buffer.append( '+' ).append( mirroredUrl ); 413 } 414 415 return buffer.toString(); 416 } 417 418 private String getTransferKey( RepositorySystemSession session, RemoteRepository repository ) 419 { 420 return getRepoKey( session, repository ); 421 } 422 423 private String getDataKey( File metadataFile ) 424 { 425 return metadataFile.getName(); 426 } 427 428 private String getTransferKey( RepositorySystemSession session, File metadataFile, 429 RemoteRepository repository ) 430 { 431 return metadataFile.getName() + '/' + getRepoKey( session, repository ); 432 } 433 434 private String getRepoKey( RepositorySystemSession session, RemoteRepository repository ) 435 { 436 StringBuilder buffer = new StringBuilder( 128 ); 437 438 Proxy proxy = repository.getProxy(); 439 if ( proxy != null ) 440 { 441 buffer.append( AuthenticationDigest.forProxy( session, repository ) ).append( '@' ); 442 buffer.append( proxy.getHost() ).append( ':' ).append( proxy.getPort() ).append( '>' ); 443 } 444 445 buffer.append( AuthenticationDigest.forRepository( session, repository ) ).append( '@' ); 446 447 buffer.append( repository.getContentType() ).append( '-' ); 448 buffer.append( repository.getId() ).append( '-' ); 449 buffer.append( normalizeRepoUrl( repository.getUrl() ) ); 450 451 return buffer.toString(); 452 } 453 454 private String normalizeRepoUrl( String url ) 455 { 456 String result = url; 457 if ( url != null && url.length() > 0 && !url.endsWith( "/" ) ) 458 { 459 result = url + '/'; 460 } 461 return result; 462 } 463 464 private String getUpdateKey( RepositorySystemSession session, File file, RemoteRepository repository ) 465 { 466 return file.getAbsolutePath() + '|' + getRepoKey( session, repository ); 467 } 468 469 private int getSessionState( RepositorySystemSession session ) 470 { 471 String mode = ConfigUtils.getString( session, "enabled", CONFIG_PROP_SESSION_STATE ); 472 if ( Boolean.parseBoolean( mode ) || "enabled".equalsIgnoreCase( mode ) ) 473 { 474 // perform update check at most once per session, regardless of update policy 475 return STATE_ENABLED; 476 } 477 else if ( "bypass".equalsIgnoreCase( mode ) ) 478 { 479 // evaluate update policy but record update in session to prevent potential future checks 480 return STATE_BYPASS; 481 } 482 else 483 { 484 // no session state at all, always evaluate update policy 485 return STATE_DISABLED; 486 } 487 } 488 489 private boolean isAlreadyUpdated( RepositorySystemSession session, Object updateKey ) 490 { 491 if ( getSessionState( session ) >= STATE_BYPASS ) 492 { 493 return false; 494 } 495 SessionData data = session.getData(); 496 Object checkedFiles = data.get( SESSION_CHECKS ); 497 if ( !( checkedFiles instanceof Map ) ) 498 { 499 return false; 500 } 501 return ( (Map<?, ?>) checkedFiles ).containsKey( updateKey ); 502 } 503 504 @SuppressWarnings( "unchecked" ) 505 private void setUpdated( RepositorySystemSession session, Object updateKey ) 506 { 507 if ( getSessionState( session ) >= STATE_DISABLED ) 508 { 509 return; 510 } 511 SessionData data = session.getData(); 512 Object checkedFiles = data.computeIfAbsent( SESSION_CHECKS, () -> new ConcurrentHashMap<>( 256 ) ); 513 ( (Map<Object, Boolean>) checkedFiles ).put( updateKey, Boolean.TRUE ); 514 } 515 516 private boolean isUpdatedRequired( RepositorySystemSession session, long lastModified, String policy ) 517 { 518 return updatePolicyAnalyzer.isUpdatedRequired( session, lastModified, policy ); 519 } 520 521 private Properties read( File touchFile ) 522 { 523 Properties props = trackingFileManager.read( touchFile ); 524 return ( props != null ) ? props : new Properties(); 525 } 526 527 public void touchArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check ) 528 { 529 requireNonNull( session, "session cannot be null" ); 530 requireNonNull( check, "check cannot be null" ); 531 File artifactFile = check.getFile(); 532 File touchFile = getArtifactTouchFile( artifactFile ); 533 534 String updateKey = getUpdateKey( session, artifactFile, check.getRepository() ); 535 String dataKey = getDataKey( check.getAuthoritativeRepository() ); 536 String transferKey = getTransferKey( session, check.getRepository() ); 537 538 setUpdated( session, updateKey ); 539 Properties props = write( touchFile, dataKey, transferKey, check.getException() ); 540 541 if ( artifactFile.exists() && !hasErrors( props ) ) 542 { 543 touchFile.delete(); 544 } 545 } 546 547 private boolean hasErrors( Properties props ) 548 { 549 for ( Object key : props.keySet() ) 550 { 551 if ( key.toString().endsWith( ERROR_KEY_SUFFIX ) ) 552 { 553 return true; 554 } 555 } 556 return false; 557 } 558 559 public void touchMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check ) 560 { 561 requireNonNull( session, "session cannot be null" ); 562 requireNonNull( check, "check cannot be null" ); 563 File metadataFile = check.getFile(); 564 File touchFile = getMetadataTouchFile( metadataFile ); 565 566 String updateKey = getUpdateKey( session, metadataFile, check.getRepository() ); 567 String dataKey = getDataKey( metadataFile ); 568 String transferKey = getTransferKey( session, metadataFile, check.getRepository() ); 569 570 setUpdated( session, updateKey ); 571 write( touchFile, dataKey, transferKey, check.getException() ); 572 } 573 574 private Properties write( File touchFile, String dataKey, String transferKey, Exception error ) 575 { 576 Map<String, String> updates = new HashMap<>(); 577 578 String timestamp = Long.toString( System.currentTimeMillis() ); 579 580 if ( error == null ) 581 { 582 updates.put( dataKey + ERROR_KEY_SUFFIX, null ); 583 updates.put( dataKey + UPDATED_KEY_SUFFIX, timestamp ); 584 updates.put( transferKey + UPDATED_KEY_SUFFIX, null ); 585 } 586 else if ( error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException ) 587 { 588 updates.put( dataKey + ERROR_KEY_SUFFIX, NOT_FOUND ); 589 updates.put( dataKey + UPDATED_KEY_SUFFIX, timestamp ); 590 updates.put( transferKey + UPDATED_KEY_SUFFIX, null ); 591 } 592 else 593 { 594 String msg = error.getMessage(); 595 if ( msg == null || msg.isEmpty() ) 596 { 597 msg = error.getClass().getSimpleName(); 598 } 599 updates.put( dataKey + ERROR_KEY_SUFFIX, msg ); 600 updates.put( dataKey + UPDATED_KEY_SUFFIX, null ); 601 updates.put( transferKey + UPDATED_KEY_SUFFIX, timestamp ); 602 } 603 604 return trackingFileManager.update( touchFile, updates ); 605 } 606 607}