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