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