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