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