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 @Deprecated 110 public DefaultUpdateCheckManager() { 111 // default ctor for ServiceLocator 112 } 113 114 @Inject 115 public DefaultUpdateCheckManager( 116 TrackingFileManager trackingFileManager, UpdatePolicyAnalyzer updatePolicyAnalyzer) { 117 setTrackingFileManager(trackingFileManager); 118 setUpdatePolicyAnalyzer(updatePolicyAnalyzer); 119 } 120 121 public void initService(ServiceLocator locator) { 122 setTrackingFileManager(locator.getService(TrackingFileManager.class)); 123 setUpdatePolicyAnalyzer(locator.getService(UpdatePolicyAnalyzer.class)); 124 } 125 126 public DefaultUpdateCheckManager setTrackingFileManager(TrackingFileManager trackingFileManager) { 127 this.trackingFileManager = requireNonNull(trackingFileManager); 128 return this; 129 } 130 131 public DefaultUpdateCheckManager setUpdatePolicyAnalyzer(UpdatePolicyAnalyzer updatePolicyAnalyzer) { 132 this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null"); 133 return this; 134 } 135 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 if (check.getLocalLastUpdated() != 0 140 && !isUpdatedRequired(session, check.getLocalLastUpdated(), check.getPolicy())) { 141 LOGGER.debug("Skipped remote request for {}, locally installed artifact up-to-date", check.getItem()); 142 143 check.setRequired(false); 144 return; 145 } 146 147 Artifact artifact = check.getItem(); 148 RemoteRepository repository = check.getRepository(); 149 150 File artifactFile = 151 requireNonNull(check.getFile(), String.format("The artifact '%s' has no file attached", artifact)); 152 153 boolean fileExists = check.isFileValid() && artifactFile.exists(); 154 155 File touchFile = getArtifactTouchFile(artifactFile); 156 Properties props = read(touchFile); 157 158 String updateKey = getUpdateKey(session, artifactFile, repository); 159 String dataKey = getDataKey(repository); 160 161 String error = getError(props, dataKey); 162 163 long lastUpdated; 164 if (error == null) { 165 if (fileExists) { 166 // last update was successful 167 lastUpdated = artifactFile.lastModified(); 168 } else { 169 // this is the first attempt ever 170 lastUpdated = TS_NEVER; 171 } 172 } else if (error.isEmpty()) { 173 // artifact did not exist 174 lastUpdated = getLastUpdated(props, dataKey); 175 } else { 176 // artifact could not be transferred 177 String transferKey = getTransferKey(session, repository); 178 lastUpdated = getLastUpdated(props, transferKey); 179 } 180 181 if (lastUpdated == TS_NEVER) { 182 check.setRequired(true); 183 } else if (isAlreadyUpdated(session, updateKey)) { 184 LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem()); 185 186 check.setRequired(false); 187 if (error != null) { 188 check.setException(newException(error, artifact, repository)); 189 } 190 } else if (isUpdatedRequired(session, lastUpdated, check.getPolicy())) { 191 check.setRequired(true); 192 } else if (fileExists) { 193 LOGGER.debug("Skipped remote request for {}, locally cached artifact up-to-date", check.getItem()); 194 195 check.setRequired(false); 196 } else { 197 int errorPolicy = Utils.getPolicy(session, artifact, repository); 198 int cacheFlag = getCacheFlag(error); 199 if ((errorPolicy & cacheFlag) != 0) { 200 check.setRequired(false); 201 check.setException(newException(error, artifact, repository)); 202 } else { 203 check.setRequired(true); 204 } 205 } 206 } 207 208 private static int getCacheFlag(String error) { 209 if (error == null || error.isEmpty()) { 210 return ResolutionErrorPolicy.CACHE_NOT_FOUND; 211 } else { 212 return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR; 213 } 214 } 215 216 private ArtifactTransferException newException(String error, Artifact artifact, RemoteRepository repository) { 217 if (error == null || error.isEmpty()) { 218 return new ArtifactNotFoundException( 219 artifact, 220 repository, 221 artifact 222 + " was not found in " + repository.getUrl() 223 + " during a previous attempt. This failure was" 224 + " cached in the local repository and" 225 + " resolution is not reattempted until the update interval of " + repository.getId() 226 + " has elapsed or updates are forced", 227 true); 228 } else { 229 return new ArtifactTransferException( 230 artifact, 231 repository, 232 artifact + " failed to transfer from " 233 + repository.getUrl() + " during a previous attempt. This failure" 234 + " was cached in the local repository and" 235 + " resolution is not reattempted until the update interval of " + repository.getId() 236 + " has elapsed or updates are forced. Original error: " + error, 237 true); 238 } 239 } 240 241 public void checkMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) { 242 requireNonNull(session, "session cannot be null"); 243 requireNonNull(check, "check cannot be null"); 244 if (check.getLocalLastUpdated() != 0 245 && !isUpdatedRequired(session, check.getLocalLastUpdated(), check.getPolicy())) { 246 LOGGER.debug("Skipped remote request for {} locally installed metadata up-to-date", check.getItem()); 247 248 check.setRequired(false); 249 return; 250 } 251 252 Metadata metadata = check.getItem(); 253 RemoteRepository repository = check.getRepository(); 254 255 File metadataFile = 256 requireNonNull(check.getFile(), String.format("The metadata '%s' has no file attached", metadata)); 257 258 boolean fileExists = check.isFileValid() && metadataFile.exists(); 259 260 File touchFile = getMetadataTouchFile(metadataFile); 261 Properties props = read(touchFile); 262 263 String updateKey = getUpdateKey(session, metadataFile, repository); 264 String dataKey = getDataKey(metadataFile); 265 266 String error = getError(props, dataKey); 267 268 long lastUpdated; 269 if (error == null) { 270 if (fileExists) { 271 // last update was successful 272 lastUpdated = getLastUpdated(props, dataKey); 273 } else { 274 // this is the first attempt ever 275 lastUpdated = TS_NEVER; 276 } 277 } else if (error.isEmpty()) { 278 // metadata did not exist 279 lastUpdated = getLastUpdated(props, dataKey); 280 } else { 281 // metadata could not be transferred 282 String transferKey = getTransferKey(session, metadataFile, repository); 283 lastUpdated = getLastUpdated(props, transferKey); 284 } 285 286 if (lastUpdated == TS_NEVER) { 287 check.setRequired(true); 288 } else if (isAlreadyUpdated(session, updateKey)) { 289 LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem()); 290 291 check.setRequired(false); 292 if (error != null) { 293 check.setException(newException(error, metadata, repository)); 294 } 295 } else if (isUpdatedRequired(session, lastUpdated, check.getPolicy())) { 296 check.setRequired(true); 297 } else if (fileExists) { 298 LOGGER.debug("Skipped remote request for {}, locally cached metadata up-to-date", check.getItem()); 299 300 check.setRequired(false); 301 } else { 302 int errorPolicy = Utils.getPolicy(session, metadata, repository); 303 int cacheFlag = getCacheFlag(error); 304 if ((errorPolicy & cacheFlag) != 0) { 305 check.setRequired(false); 306 check.setException(newException(error, metadata, repository)); 307 } else { 308 check.setRequired(true); 309 } 310 } 311 } 312 313 private MetadataTransferException newException(String error, Metadata metadata, RemoteRepository repository) { 314 if (error == null || error.isEmpty()) { 315 return new MetadataNotFoundException( 316 metadata, 317 repository, 318 metadata + " was not found in " 319 + repository.getUrl() + " during a previous attempt." 320 + " This failure was cached in the local repository and" 321 + " resolution is not be reattempted until the update interval of " + repository.getId() 322 + " has elapsed or updates are forced", 323 true); 324 } else { 325 return new MetadataTransferException( 326 metadata, 327 repository, 328 metadata + " failed to transfer from " 329 + repository.getUrl() + " during a previous attempt." 330 + " This failure was cached in the local repository and" 331 + " resolution will not be reattempted until the update interval of " + repository.getId() 332 + " has elapsed or updates are forced. Original error: " + error, 333 true); 334 } 335 } 336 337 private long getLastUpdated(Properties props, String key) { 338 String value = props.getProperty(key + UPDATED_KEY_SUFFIX, ""); 339 try { 340 return (value.length() > 0) ? Long.parseLong(value) : TS_UNKNOWN; 341 } catch (NumberFormatException e) { 342 LOGGER.debug("Cannot parse last updated date {}, ignoring it", value, e); 343 return TS_UNKNOWN; 344 } 345 } 346 347 private String getError(Properties props, String key) { 348 return props.getProperty(key + ERROR_KEY_SUFFIX); 349 } 350 351 private File getArtifactTouchFile(File artifactFile) { 352 return new File(artifactFile.getPath() + UPDATED_KEY_SUFFIX); 353 } 354 355 private File getMetadataTouchFile(File metadataFile) { 356 return new File(metadataFile.getParent(), "resolver-status.properties"); 357 } 358 359 private String getDataKey(RemoteRepository repository) { 360 Set<String> mirroredUrls = Collections.emptySet(); 361 if (repository.isRepositoryManager()) { 362 mirroredUrls = new TreeSet<>(); 363 for (RemoteRepository mirroredRepository : repository.getMirroredRepositories()) { 364 mirroredUrls.add(normalizeRepoUrl(mirroredRepository.getUrl())); 365 } 366 } 367 368 StringBuilder buffer = new StringBuilder(1024); 369 370 buffer.append(normalizeRepoUrl(repository.getUrl())); 371 for (String mirroredUrl : mirroredUrls) { 372 buffer.append('+').append(mirroredUrl); 373 } 374 375 return buffer.toString(); 376 } 377 378 private String getTransferKey(RepositorySystemSession session, RemoteRepository repository) { 379 return getRepoKey(session, repository); 380 } 381 382 private String getDataKey(File metadataFile) { 383 return metadataFile.getName(); 384 } 385 386 private String getTransferKey(RepositorySystemSession session, File metadataFile, RemoteRepository repository) { 387 return metadataFile.getName() + '/' + getRepoKey(session, repository); 388 } 389 390 private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) { 391 StringBuilder buffer = new StringBuilder(128); 392 393 Proxy proxy = repository.getProxy(); 394 if (proxy != null) { 395 buffer.append(AuthenticationDigest.forProxy(session, repository)).append('@'); 396 buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>'); 397 } 398 399 buffer.append(AuthenticationDigest.forRepository(session, repository)).append('@'); 400 401 buffer.append(repository.getContentType()).append('-'); 402 buffer.append(repository.getId()).append('-'); 403 buffer.append(normalizeRepoUrl(repository.getUrl())); 404 405 return buffer.toString(); 406 } 407 408 private String normalizeRepoUrl(String url) { 409 String result = url; 410 if (url != null && url.length() > 0 && !url.endsWith("/")) { 411 result = url + '/'; 412 } 413 return result; 414 } 415 416 private String getUpdateKey(RepositorySystemSession session, File file, RemoteRepository repository) { 417 return file.getAbsolutePath() + '|' + getRepoKey(session, repository); 418 } 419 420 private int getSessionState(RepositorySystemSession session) { 421 String mode = ConfigUtils.getString(session, "enabled", CONFIG_PROP_SESSION_STATE); 422 if (Boolean.parseBoolean(mode) || "enabled".equalsIgnoreCase(mode)) { 423 // perform update check at most once per session, regardless of update policy 424 return STATE_ENABLED; 425 } else if ("bypass".equalsIgnoreCase(mode)) { 426 // evaluate update policy but record update in session to prevent potential future checks 427 return STATE_BYPASS; 428 } else { 429 // no session state at all, always evaluate update policy 430 return STATE_DISABLED; 431 } 432 } 433 434 private boolean isAlreadyUpdated(RepositorySystemSession session, Object updateKey) { 435 if (getSessionState(session) >= STATE_BYPASS) { 436 return false; 437 } 438 SessionData data = session.getData(); 439 Object checkedFiles = data.get(SESSION_CHECKS); 440 if (!(checkedFiles instanceof Map)) { 441 return false; 442 } 443 return ((Map<?, ?>) checkedFiles).containsKey(updateKey); 444 } 445 446 @SuppressWarnings("unchecked") 447 private void setUpdated(RepositorySystemSession session, Object updateKey) { 448 if (getSessionState(session) >= STATE_DISABLED) { 449 return; 450 } 451 SessionData data = session.getData(); 452 Object checkedFiles = data.computeIfAbsent(SESSION_CHECKS, () -> new ConcurrentHashMap<>(256)); 453 ((Map<Object, Boolean>) checkedFiles).put(updateKey, Boolean.TRUE); 454 } 455 456 private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) { 457 return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy); 458 } 459 460 private Properties read(File touchFile) { 461 Properties props = trackingFileManager.read(touchFile); 462 return (props != null) ? props : new Properties(); 463 } 464 465 public void touchArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) { 466 requireNonNull(session, "session cannot be null"); 467 requireNonNull(check, "check cannot be null"); 468 File artifactFile = check.getFile(); 469 File touchFile = getArtifactTouchFile(artifactFile); 470 471 String updateKey = getUpdateKey(session, artifactFile, check.getRepository()); 472 String dataKey = getDataKey(check.getAuthoritativeRepository()); 473 String transferKey = getTransferKey(session, check.getRepository()); 474 475 setUpdated(session, updateKey); 476 Properties props = write(touchFile, dataKey, transferKey, check.getException()); 477 478 if (artifactFile.exists() && !hasErrors(props)) { 479 touchFile.delete(); 480 } 481 } 482 483 private boolean hasErrors(Properties props) { 484 for (Object key : props.keySet()) { 485 if (key.toString().endsWith(ERROR_KEY_SUFFIX)) { 486 return true; 487 } 488 } 489 return false; 490 } 491 492 public void touchMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) { 493 requireNonNull(session, "session cannot be null"); 494 requireNonNull(check, "check cannot be null"); 495 File metadataFile = check.getFile(); 496 File touchFile = getMetadataTouchFile(metadataFile); 497 498 String updateKey = getUpdateKey(session, metadataFile, check.getRepository()); 499 String dataKey = getDataKey(metadataFile); 500 String transferKey = getTransferKey(session, metadataFile, check.getRepository()); 501 502 setUpdated(session, updateKey); 503 write(touchFile, dataKey, transferKey, check.getException()); 504 } 505 506 private Properties write(File touchFile, String dataKey, String transferKey, Exception error) { 507 Map<String, String> updates = new HashMap<>(); 508 509 String timestamp = Long.toString(System.currentTimeMillis()); 510 511 if (error == null) { 512 updates.put(dataKey + ERROR_KEY_SUFFIX, null); 513 updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp); 514 updates.put(transferKey + UPDATED_KEY_SUFFIX, null); 515 } else if (error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException) { 516 updates.put(dataKey + ERROR_KEY_SUFFIX, NOT_FOUND); 517 updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp); 518 updates.put(transferKey + UPDATED_KEY_SUFFIX, null); 519 } else { 520 String msg = error.getMessage(); 521 if (msg == null || msg.isEmpty()) { 522 msg = error.getClass().getSimpleName(); 523 } 524 updates.put(dataKey + ERROR_KEY_SUFFIX, msg); 525 updates.put(dataKey + UPDATED_KEY_SUFFIX, null); 526 updates.put(transferKey + UPDATED_KEY_SUFFIX, timestamp); 527 } 528 529 return trackingFileManager.update(touchFile, updates); 530 } 531}