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