001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.internal.impl; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023import javax.inject.Singleton; 024 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.Map; 030import java.util.Properties; 031import java.util.Set; 032import java.util.TreeSet; 033import java.util.concurrent.ConcurrentHashMap; 034 035import org.eclipse.aether.ConfigurationProperties; 036import org.eclipse.aether.Keys; 037import org.eclipse.aether.RepositorySystemSession; 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.io.PathProcessor; 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 056import static java.util.Objects.requireNonNull; 057 058/** 059 */ 060@Singleton 061@Named 062public class DefaultUpdateCheckManager implements UpdateCheckManager { 063 064 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUpdateCheckManager.class); 065 066 private static final String UPDATED_KEY_SUFFIX = ".lastUpdated"; 067 068 private static final String ERROR_KEY_SUFFIX = ".error"; 069 070 private static final String NOT_FOUND = ""; 071 072 // instance bound private key 073 static final Object SESSION_CHECKS = Keys.of(new Object() { 074 @Override 075 public String toString() { 076 return "updateCheckManager.checks"; 077 } 078 }); 079 080 /** 081 * Manages the session state, i.e. influences if the same download requests to artifacts/metadata will happen 082 * multiple times within the same RepositorySystemSession. If "enabled" will enable the session state. If "bypass" 083 * will enable bypassing (i.e. store all artifact ids/metadata ids which have been updates but not evaluating 084 * those). All other values lead to disabling the session state completely. 085 * 086 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 087 * @configurationType {@link java.lang.String} 088 * @configurationDefaultValue {@link #DEFAULT_SESSION_STATE} 089 */ 090 public static final String CONFIG_PROP_SESSION_STATE = 091 ConfigurationProperties.PREFIX_AETHER + "updateCheckManager.sessionState"; 092 093 public static final String DEFAULT_SESSION_STATE = "enabled"; 094 095 private static final int STATE_ENABLED = 0; 096 097 private static final int STATE_BYPASS = 1; 098 099 private static final int STATE_DISABLED = 2; 100 101 /** 102 * This "last modified" timestamp is used when no local file is present, signaling "first attempt" to cache a file, 103 * but as it is not present, outcome is simply always "go get it". 104 * <p> 105 * Its meaning is "we never downloaded it", so go grab it. 106 */ 107 private static final long TS_NEVER = 0L; 108 109 /** 110 * This "last modified" timestamp is returned by {@link #getLastUpdated(Properties, String)} method when the 111 * timestamp entry is not found (due properties file not present or key not present in properties file, irrelevant). 112 * It means that the cached file (artifact or metadata) is present, but we cannot tell when was it downloaded. In 113 * this case, it is {@link UpdatePolicyAnalyzer} applying in-effect policy, that decide is update (re-download) 114 * needed or not. For example, if policy is "never", we should not re-download the file. 115 * <p> 116 * Its meaning is "we downloaded it, but have no idea when", so let the policy decide its fate. 117 */ 118 private static final long TS_UNKNOWN = 1L; 119 120 private final TrackingFileManager trackingFileManager; 121 122 private final UpdatePolicyAnalyzer updatePolicyAnalyzer; 123 124 private final PathProcessor pathProcessor; 125 126 @Inject 127 public DefaultUpdateCheckManager( 128 TrackingFileManager trackingFileManager, 129 UpdatePolicyAnalyzer updatePolicyAnalyzer, 130 PathProcessor pathProcessor) { 131 this.trackingFileManager = requireNonNull(trackingFileManager, "tracking file manager cannot be null"); 132 this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null"); 133 this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null"); 134 } 135 136 @Override 137 public void checkArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) { 138 requireNonNull(session, "session cannot be null"); 139 requireNonNull(check, "check cannot be null"); 140 final String updatePolicy = check.getArtifactPolicy(); 141 if (check.getLocalLastUpdated() != 0 142 && !isUpdatedRequired(session, check.getLocalLastUpdated(), updatePolicy)) { 143 LOGGER.debug("Skipped remote request for {}, locally installed artifact up-to-date", check.getItem()); 144 145 check.setRequired(false); 146 return; 147 } 148 149 Artifact artifact = check.getItem(); 150 RemoteRepository repository = check.getRepository(); 151 152 Path artifactPath = 153 requireNonNull(check.getPath(), String.format("The artifact '%s' has no file attached", artifact)); 154 155 boolean fileExists = check.isFileValid() && Files.exists(artifactPath); 156 157 Path touchPath = getArtifactTouchFile(artifactPath); 158 Properties props = read(touchPath); 159 160 String updateKey = getUpdateKey(session, artifactPath, repository); 161 String dataKey = getDataKey(repository); 162 163 String error = getError(props, dataKey); 164 165 long lastUpdated; 166 if (error == null) { 167 if (fileExists) { 168 // last update was successful 169 lastUpdated = pathProcessor.lastModified(artifactPath, 0L); 170 } else { 171 // this is the first attempt ever 172 lastUpdated = TS_NEVER; 173 } 174 } else if (error.isEmpty()) { 175 // artifact did not exist 176 lastUpdated = getLastUpdated(props, dataKey); 177 } else { 178 // artifact could not be transferred 179 String transferKey = getTransferKey(session, repository); 180 lastUpdated = getLastUpdated(props, transferKey); 181 } 182 183 if (lastUpdated == TS_NEVER) { 184 check.setRequired(true); 185 } else if (isAlreadyUpdated(session, updateKey)) { 186 LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem()); 187 188 check.setRequired(false); 189 if (error != null) { 190 check.setException(newException(error, artifact, repository)); 191 } 192 } else if (isUpdatedRequired(session, lastUpdated, updatePolicy)) { 193 check.setRequired(true); 194 } else if (fileExists) { 195 LOGGER.debug("Skipped remote request for {}, locally cached artifact up-to-date", check.getItem()); 196 197 check.setRequired(false); 198 } else { 199 int errorPolicy = Utils.getPolicy(session, artifact, repository); 200 int cacheFlag = getCacheFlag(error); 201 if ((errorPolicy & cacheFlag) != 0) { 202 check.setRequired(false); 203 check.setException(newException(error, artifact, repository)); 204 } else { 205 check.setRequired(true); 206 } 207 } 208 } 209 210 private static int getCacheFlag(String error) { 211 if (error == null || error.isEmpty()) { 212 return ResolutionErrorPolicy.CACHE_NOT_FOUND; 213 } else { 214 return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR; 215 } 216 } 217 218 private ArtifactTransferException newException(String error, Artifact artifact, RemoteRepository repository) { 219 if (error == null || error.isEmpty()) { 220 return new ArtifactNotFoundException( 221 artifact, 222 repository, 223 artifact 224 + " was not found in " + repository.getUrl() 225 + " during a previous attempt. This failure was" 226 + " cached in the local repository and" 227 + " resolution is not reattempted until the update interval of " + repository.getId() 228 + " has elapsed or updates are forced", 229 true); 230 } else { 231 return new ArtifactTransferException( 232 artifact, 233 repository, 234 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, 239 true); 240 } 241 } 242 243 @Override 244 public void checkMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) { 245 requireNonNull(session, "session cannot be null"); 246 requireNonNull(check, "check cannot be null"); 247 final String updatePolicy = check.getMetadataPolicy(); 248 if (check.getLocalLastUpdated() != 0 249 && !isUpdatedRequired(session, check.getLocalLastUpdated(), updatePolicy)) { 250 LOGGER.debug("Skipped remote request for {} locally installed metadata up-to-date", check.getItem()); 251 252 check.setRequired(false); 253 return; 254 } 255 256 Metadata metadata = check.getItem(); 257 RemoteRepository repository = check.getRepository(); 258 259 Path metadataPath = 260 requireNonNull(check.getPath(), String.format("The metadata '%s' has no file attached", metadata)); 261 262 boolean fileExists = check.isFileValid() && Files.exists(metadataPath); 263 264 Path touchPath = getMetadataTouchFile(metadataPath); 265 Properties props = read(touchPath); 266 267 String updateKey = getUpdateKey(session, metadataPath, repository); 268 String dataKey = getDataKey(metadataPath); 269 270 String error = getError(props, dataKey); 271 272 long lastUpdated; 273 if (error == null) { 274 if (fileExists) { 275 // last update was successful 276 lastUpdated = getLastUpdated(props, dataKey); 277 } else { 278 // this is the first attempt ever 279 lastUpdated = TS_NEVER; 280 } 281 } else if (error.isEmpty()) { 282 // metadata did not exist 283 lastUpdated = getLastUpdated(props, dataKey); 284 } else { 285 // metadata could not be transferred 286 String transferKey = getTransferKey(session, metadataPath, repository); 287 lastUpdated = getLastUpdated(props, transferKey); 288 } 289 290 if (lastUpdated == TS_NEVER) { 291 check.setRequired(true); 292 } else if (isAlreadyUpdated(session, updateKey)) { 293 LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem()); 294 295 check.setRequired(false); 296 if (error != null) { 297 check.setException(newException(error, metadata, repository)); 298 } 299 } else if (isUpdatedRequired(session, lastUpdated, updatePolicy)) { 300 check.setRequired(true); 301 } else if (fileExists) { 302 LOGGER.debug("Skipped remote request for {}, locally cached metadata up-to-date", check.getItem()); 303 304 check.setRequired(false); 305 } else { 306 int errorPolicy = Utils.getPolicy(session, metadata, repository); 307 int cacheFlag = getCacheFlag(error); 308 if ((errorPolicy & cacheFlag) != 0) { 309 check.setRequired(false); 310 check.setException(newException(error, metadata, repository)); 311 } else { 312 check.setRequired(true); 313 } 314 } 315 } 316 317 private MetadataTransferException newException(String error, Metadata metadata, RemoteRepository repository) { 318 if (error == null || error.isEmpty()) { 319 return new MetadataNotFoundException( 320 metadata, 321 repository, 322 metadata + " was not found in " 323 + repository.getUrl() + " during a previous attempt." 324 + " This failure was cached in the local repository and" 325 + " resolution is not be reattempted until the update interval of " + repository.getId() 326 + " has elapsed or updates are forced", 327 true); 328 } else { 329 return new MetadataTransferException( 330 metadata, 331 repository, 332 metadata + " failed to transfer from " 333 + repository.getUrl() + " during a previous attempt." 334 + " This failure was cached in the local repository and" 335 + " resolution will not be reattempted until the update interval of " + repository.getId() 336 + " has elapsed or updates are forced. Original error: " + error, 337 true); 338 } 339 } 340 341 private long getLastUpdated(Properties props, String key) { 342 String value = props.getProperty(key + UPDATED_KEY_SUFFIX, ""); 343 try { 344 return (!value.isEmpty()) ? Long.parseLong(value) : TS_UNKNOWN; 345 } catch (NumberFormatException e) { 346 LOGGER.debug("Cannot parse last updated date {}, ignoring it", value, e); 347 return TS_UNKNOWN; 348 } 349 } 350 351 private String getError(Properties props, String key) { 352 return props.getProperty(key + ERROR_KEY_SUFFIX); 353 } 354 355 private Path getArtifactTouchFile(Path artifactPath) { 356 return artifactPath.getParent().resolve(artifactPath.getFileName() + UPDATED_KEY_SUFFIX); 357 } 358 359 private Path getMetadataTouchFile(Path metadataPath) { 360 return metadataPath.getParent().resolve("resolver-status.properties"); 361 } 362 363 private String getDataKey(RemoteRepository repository) { 364 Set<String> mirroredUrls = Collections.emptySet(); 365 if (repository.isRepositoryManager()) { 366 mirroredUrls = new TreeSet<>(); 367 for (RemoteRepository mirroredRepository : repository.getMirroredRepositories()) { 368 mirroredUrls.add(normalizeRepoUrl(mirroredRepository.getUrl())); 369 } 370 } 371 372 StringBuilder buffer = new StringBuilder(1024); 373 374 buffer.append(normalizeRepoUrl(repository.getUrl())); 375 for (String mirroredUrl : mirroredUrls) { 376 buffer.append('+').append(mirroredUrl); 377 } 378 379 return buffer.toString(); 380 } 381 382 private String getTransferKey(RepositorySystemSession session, RemoteRepository repository) { 383 return getRepoKey(session, repository); 384 } 385 386 private String getDataKey(Path metadataPath) { 387 return metadataPath.getFileName().toString(); 388 } 389 390 private String getTransferKey(RepositorySystemSession session, Path metadataPath, RemoteRepository repository) { 391 return metadataPath.getFileName().toString() + '/' + getRepoKey(session, repository); 392 } 393 394 private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) { 395 StringBuilder buffer = new StringBuilder(128); 396 397 Proxy proxy = repository.getProxy(); 398 if (proxy != null) { 399 buffer.append(AuthenticationDigest.forProxy(session, repository)).append('@'); 400 buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>'); 401 } 402 403 buffer.append(AuthenticationDigest.forRepository(session, repository)).append('@'); 404 405 buffer.append(repository.getContentType()).append('-'); 406 buffer.append(repository.getId()).append('-'); 407 buffer.append(normalizeRepoUrl(repository.getUrl())); 408 409 return buffer.toString(); 410 } 411 412 private String normalizeRepoUrl(String url) { 413 String result = url; 414 if (url != null && !url.isEmpty() && !url.endsWith("/")) { 415 result = url + '/'; 416 } 417 return result; 418 } 419 420 private String getUpdateKey(RepositorySystemSession session, Path path, RemoteRepository repository) { 421 return path.toAbsolutePath() + "|" + getRepoKey(session, repository); 422 } 423 424 private int getSessionState(RepositorySystemSession session) { 425 String mode = ConfigUtils.getString(session, DEFAULT_SESSION_STATE, CONFIG_PROP_SESSION_STATE); 426 if (Boolean.parseBoolean(mode) || "enabled".equalsIgnoreCase(mode)) { 427 // perform update check at most once per session, regardless of update policy 428 return STATE_ENABLED; 429 } else if ("bypass".equalsIgnoreCase(mode)) { 430 // evaluate update policy but record update in session to prevent potential future checks 431 return STATE_BYPASS; 432 } else { 433 // no session state at all, always evaluate update policy 434 return STATE_DISABLED; 435 } 436 } 437 438 private boolean isAlreadyUpdated(RepositorySystemSession session, String updateKey) { 439 if (getSessionState(session) >= STATE_BYPASS) { 440 return false; 441 } 442 Object checkedFiles = session.getData().get(SESSION_CHECKS); 443 if (!(checkedFiles instanceof Map)) { 444 return false; 445 } 446 return ((Map<?, ?>) checkedFiles).containsKey(updateKey); 447 } 448 449 @SuppressWarnings("unchecked") 450 private void setUpdated(RepositorySystemSession session, String updateKey) { 451 if (getSessionState(session) >= STATE_DISABLED) { 452 return; 453 } 454 Object checkedFiles = session.getData().computeIfAbsent(SESSION_CHECKS, () -> new ConcurrentHashMap<>(256)); 455 ((Map<String, Boolean>) checkedFiles).put(updateKey, Boolean.TRUE); 456 } 457 458 private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) { 459 return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy); 460 } 461 462 private Properties read(Path touchPath) { 463 Properties props = trackingFileManager.read(touchPath); 464 return (props != null) ? props : new Properties(); 465 } 466 467 @Override 468 public void touchArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) { 469 requireNonNull(session, "session cannot be null"); 470 requireNonNull(check, "check cannot be null"); 471 Path artifactPath = check.getPath(); 472 Path touchPath = getArtifactTouchFile(artifactPath); 473 474 String updateKey = getUpdateKey(session, artifactPath, check.getRepository()); 475 String dataKey = getDataKey(check.getAuthoritativeRepository()); 476 String transferKey = getTransferKey(session, check.getRepository()); 477 478 setUpdated(session, updateKey); 479 Properties props = write(touchPath, dataKey, transferKey, check.getException()); 480 481 if (Files.exists(artifactPath) && !hasErrors(props)) { 482 trackingFileManager.delete(touchPath); 483 } 484 } 485 486 private boolean hasErrors(Properties props) { 487 for (Object key : props.keySet()) { 488 if (key.toString().endsWith(ERROR_KEY_SUFFIX)) { 489 return true; 490 } 491 } 492 return false; 493 } 494 495 @Override 496 public void touchMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) { 497 requireNonNull(session, "session cannot be null"); 498 requireNonNull(check, "check cannot be null"); 499 Path metadataPath = check.getPath(); 500 Path touchPath = getMetadataTouchFile(metadataPath); 501 502 String updateKey = getUpdateKey(session, metadataPath, check.getRepository()); 503 String dataKey = getDataKey(metadataPath); 504 String transferKey = getTransferKey(session, metadataPath, check.getRepository()); 505 506 setUpdated(session, updateKey); 507 write(touchPath, dataKey, transferKey, check.getException()); 508 } 509 510 private Properties write(Path touchPath, String dataKey, String transferKey, Exception error) { 511 Map<String, String> updates = new HashMap<>(); 512 513 String timestamp = Long.toString(System.currentTimeMillis()); 514 515 if (error == null) { 516 updates.put(dataKey + ERROR_KEY_SUFFIX, null); 517 updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp); 518 updates.put(transferKey + UPDATED_KEY_SUFFIX, null); 519 } else if (error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException) { 520 updates.put(dataKey + ERROR_KEY_SUFFIX, NOT_FOUND); 521 updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp); 522 updates.put(transferKey + UPDATED_KEY_SUFFIX, null); 523 } else { 524 String msg = error.getMessage(); 525 if (msg == null || msg.isEmpty()) { 526 msg = error.getClass().getSimpleName(); 527 } 528 updates.put(dataKey + ERROR_KEY_SUFFIX, msg); 529 updates.put(dataKey + UPDATED_KEY_SUFFIX, null); 530 updates.put(transferKey + UPDATED_KEY_SUFFIX, timestamp); 531 } 532 533 return trackingFileManager.update(touchPath, updates); 534 } 535}