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