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