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