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