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 LOGGER.debug( "Skipped remote request for {}, already updated during this session", check.getItem() ); 165 166 check.setRequired( false ); 167 if ( error != null ) 168 { 169 check.setException( newException( error, artifact, repository ) ); 170 } 171 } 172 else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) ) 173 { 174 check.setRequired( true ); 175 } 176 else if ( fileExists ) 177 { 178 LOGGER.debug( "Skipped remote request for {}, locally cached artifact up-to-date", check.getItem() ); 179 180 check.setRequired( false ); 181 } 182 else 183 { 184 int errorPolicy = Utils.getPolicy( session, artifact, repository ); 185 int cacheFlag = getCacheFlag( error ); 186 if ( ( errorPolicy & cacheFlag ) != 0 ) 187 { 188 check.setRequired( false ); 189 check.setException( newException( error, artifact, repository ) ); 190 } 191 else 192 { 193 check.setRequired( true ); 194 } 195 } 196 } 197 198 private static int getCacheFlag( String error ) 199 { 200 if ( error == null || error.length() <= 0 ) 201 { 202 return ResolutionErrorPolicy.CACHE_NOT_FOUND; 203 } 204 else 205 { 206 return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR; 207 } 208 } 209 210 private ArtifactTransferException newException( String error, Artifact artifact, RemoteRepository repository ) 211 { 212 if ( error == null || error.length() <= 0 ) 213 { 214 return new ArtifactNotFoundException( artifact, repository, artifact 215 + " was not found in " + repository.getUrl() + " during a previous attempt. This failure was" 216 + " cached in the local repository and" 217 + " resolution is not reattempted until the update interval of " + repository.getId() 218 + " has elapsed or updates are forced", true ); 219 } 220 else 221 { 222 return new ArtifactTransferException( artifact, repository, artifact + " failed to transfer from " 223 + repository.getUrl() + " during a previous attempt. This failure" 224 + " was cached in the local repository and" 225 + " resolution is not reattempted until the update interval of " + repository.getId() 226 + " has elapsed or updates are forced. Original error: " + error, true ); 227 } 228 } 229 230 public void checkMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check ) 231 { 232 if ( check.getLocalLastUpdated() != 0 233 && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) ) 234 { 235 LOGGER.debug( "Skipped remote request for {} locally installed metadata up-to-date", check.getItem() ); 236 237 check.setRequired( false ); 238 return; 239 } 240 241 Metadata metadata = check.getItem(); 242 RemoteRepository repository = check.getRepository(); 243 244 File metadataFile = requireNonNull( check.getFile(), String.format( "The metadata '%s' has no file attached", 245 metadata ) ); 246 247 boolean fileExists = check.isFileValid() && metadataFile.exists(); 248 249 File touchFile = getTouchFile( metadata, metadataFile ); 250 Properties props = read( touchFile ); 251 252 String updateKey = getUpdateKey( session, metadataFile, repository ); 253 String dataKey = getDataKey( metadata, metadataFile, check.getAuthoritativeRepository() ); 254 255 String error = getError( props, dataKey ); 256 257 long lastUpdated; 258 if ( error == null ) 259 { 260 if ( fileExists ) 261 { 262 // last update was successful 263 lastUpdated = getLastUpdated( props, dataKey ); 264 } 265 else 266 { 267 // this is the first attempt ever 268 lastUpdated = 0L; 269 } 270 } 271 else if ( error.length() <= 0 ) 272 { 273 // metadata did not exist 274 lastUpdated = getLastUpdated( props, dataKey ); 275 } 276 else 277 { 278 // metadata could not be transferred 279 String transferKey = getTransferKey( session, metadata, metadataFile, repository ); 280 lastUpdated = getLastUpdated( props, transferKey ); 281 } 282 283 if ( lastUpdated == 0L ) 284 { 285 check.setRequired( true ); 286 } 287 else if ( isAlreadyUpdated( session, updateKey ) ) 288 { 289 LOGGER.debug( "Skipped remote request for {}, already updated during this session", check.getItem() ); 290 291 check.setRequired( false ); 292 if ( error != null ) 293 { 294 check.setException( newException( error, metadata, repository ) ); 295 } 296 } 297 else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) ) 298 { 299 check.setRequired( true ); 300 } 301 else if ( fileExists ) 302 { 303 LOGGER.debug( "Skipped remote request for {}, locally cached metadata up-to-date", check.getItem() ); 304 305 check.setRequired( false ); 306 } 307 else 308 { 309 int errorPolicy = Utils.getPolicy( session, metadata, repository ); 310 int cacheFlag = getCacheFlag( error ); 311 if ( ( errorPolicy & cacheFlag ) != 0 ) 312 { 313 check.setRequired( false ); 314 check.setException( newException( error, metadata, repository ) ); 315 } 316 else 317 { 318 check.setRequired( true ); 319 } 320 } 321 } 322 323 private MetadataTransferException newException( String error, Metadata metadata, RemoteRepository repository ) 324 { 325 if ( error == null || error.length() <= 0 ) 326 { 327 return new MetadataNotFoundException( metadata, repository, metadata + " was not found in " 328 + repository.getUrl() + " during a previous attempt." 329 + " This failure was cached in the local repository and" 330 + " resolution is not be reattempted until the update interval of " + repository.getId() 331 + " has elapsed or updates are forced", true ); 332 } 333 else 334 { 335 return new MetadataTransferException( metadata, repository, metadata + "failed to transfer from " 336 + repository.getUrl() + " during a previous attempt." 337 + " This failure was cached in the local repository and" 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 last updated date {}, ignoring it", 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, "enabled", CONFIG_PROP_SESSION_STATE ); 450 if ( Boolean.parseBoolean( mode ) || "enabled".equalsIgnoreCase( 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}