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