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