1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.internal.impl;
20
21 import javax.inject.Inject;
22 import javax.inject.Named;
23 import javax.inject.Singleton;
24
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.Properties;
31 import java.util.Set;
32 import java.util.TreeSet;
33 import java.util.concurrent.ConcurrentHashMap;
34
35 import org.eclipse.aether.ConfigurationProperties;
36 import org.eclipse.aether.Keys;
37 import org.eclipse.aether.RepositorySystemSession;
38 import org.eclipse.aether.artifact.Artifact;
39 import org.eclipse.aether.impl.UpdateCheck;
40 import org.eclipse.aether.impl.UpdateCheckManager;
41 import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
42 import org.eclipse.aether.metadata.Metadata;
43 import org.eclipse.aether.repository.AuthenticationDigest;
44 import org.eclipse.aether.repository.Proxy;
45 import org.eclipse.aether.repository.RemoteRepository;
46 import org.eclipse.aether.resolution.ResolutionErrorPolicy;
47 import org.eclipse.aether.spi.io.PathProcessor;
48 import org.eclipse.aether.transfer.ArtifactNotFoundException;
49 import org.eclipse.aether.transfer.ArtifactTransferException;
50 import org.eclipse.aether.transfer.MetadataNotFoundException;
51 import org.eclipse.aether.transfer.MetadataTransferException;
52 import org.eclipse.aether.util.ConfigUtils;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 import static java.util.Objects.requireNonNull;
57
58
59
60 @Singleton
61 @Named
62 public class DefaultUpdateCheckManager implements UpdateCheckManager {
63
64 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUpdateCheckManager.class);
65
66 private static final String UPDATED_KEY_SUFFIX = ".lastUpdated";
67
68 private static final String ERROR_KEY_SUFFIX = ".error";
69
70 private static final String NOT_FOUND = "";
71
72
73 static final Object SESSION_CHECKS = Keys.of(new Object() {
74 @Override
75 public String toString() {
76 return "updateCheckManager.checks";
77 }
78 });
79
80
81 static final Object SESSION_NOT_FOUNDS = Keys.of(new Object() {
82 @Override
83 public String toString() {
84 return "updateCheckManager.notFounds";
85 }
86 });
87
88
89
90
91
92
93
94
95
96
97
98 public static final String CONFIG_PROP_SESSION_STATE =
99 ConfigurationProperties.PREFIX_AETHER + "updateCheckManager.sessionState";
100
101 public static final String DEFAULT_SESSION_STATE = "enabled";
102
103 private static final int STATE_ENABLED = 0;
104
105 private static final int STATE_BYPASS = 1;
106
107 private static final int STATE_DISABLED = 2;
108
109
110
111
112
113
114
115 private static final long TS_NEVER = 0L;
116
117
118
119
120
121
122
123
124
125
126 private static final long TS_UNKNOWN = 1L;
127
128 private final TrackingFileManager trackingFileManager;
129
130 private final UpdatePolicyAnalyzer updatePolicyAnalyzer;
131
132 private final PathProcessor pathProcessor;
133
134 @Inject
135 public DefaultUpdateCheckManager(
136 TrackingFileManager trackingFileManager,
137 UpdatePolicyAnalyzer updatePolicyAnalyzer,
138 PathProcessor pathProcessor) {
139 this.trackingFileManager = requireNonNull(trackingFileManager, "tracking file manager cannot be null");
140 this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null");
141 this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null");
142 }
143
144 @Override
145 public void checkArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
146 requireNonNull(session, "session cannot be null");
147 requireNonNull(check, "check cannot be null");
148 final String updatePolicy = check.getArtifactPolicy();
149 if (check.getLocalLastUpdated() != 0
150 && !isUpdatedRequired(session, check.getLocalLastUpdated(), updatePolicy)) {
151 LOGGER.debug("Skipped remote request for {}, locally installed artifact up-to-date", check.getItem());
152
153 check.setRequired(false);
154 return;
155 }
156
157 Artifact artifact = check.getItem();
158 RemoteRepository repository = check.getRepository();
159
160 Path artifactPath =
161 requireNonNull(check.getPath(), String.format("The artifact '%s' has no file attached", artifact));
162
163 boolean fileExists = check.isFileValid() && Files.exists(artifactPath);
164
165 Path touchPath = getArtifactTouchFile(artifactPath);
166 Properties props = read(touchPath);
167
168 String updateKey = getUpdateKey(session, artifactPath, repository);
169 String dataKey = getDataKey(repository);
170
171 String error = getError(props, dataKey);
172
173 long lastUpdated;
174 if (error == null) {
175 if (fileExists) {
176
177 lastUpdated = pathProcessor.lastModified(artifactPath, 0L);
178 } else {
179
180 lastUpdated = TS_NEVER;
181 }
182 } else if (error.isEmpty()) {
183
184 lastUpdated = getLastUpdated(props, dataKey);
185 } else {
186
187 String transferKey = getTransferKey(session, repository);
188 lastUpdated = getLastUpdated(props, transferKey);
189 }
190
191 if (lastUpdated == TS_NEVER) {
192 check.setRequired(true);
193 } else if (isAlreadyUpdated(session, updateKey)) {
194 LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem());
195
196 check.setRequired(false);
197 if (error != null) {
198 check.setException(newException(error, artifact, repository));
199 }
200 } else if (isUpdatedRequired(session, lastUpdated, updatePolicy)) {
201 check.setRequired(true);
202 } else if (fileExists) {
203 LOGGER.debug("Skipped remote request for {}, locally cached artifact up-to-date", check.getItem());
204
205 check.setRequired(false);
206 } else {
207 int errorPolicy = Utils.getPolicy(session, artifact, repository);
208 int cacheFlag = getCacheFlag(error);
209 if ((errorPolicy & cacheFlag) != 0) {
210 check.setRequired(false);
211 check.setException(newException(error, artifact, repository));
212 } else {
213 check.setRequired(true);
214 }
215 }
216 }
217
218 private static int getCacheFlag(String error) {
219 if (error == null || error.isEmpty()) {
220 return ResolutionErrorPolicy.CACHE_NOT_FOUND;
221 } else {
222 return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR;
223 }
224 }
225
226 private ArtifactTransferException newException(String error, Artifact artifact, RemoteRepository repository) {
227 if (error == null || error.isEmpty()) {
228 return new ArtifactNotFoundException(
229 artifact,
230 repository,
231 artifact
232 + " was not found in " + repository.getUrl()
233 + " during a previous attempt. This failure was"
234 + " 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",
237 true);
238 } else {
239 return new ArtifactTransferException(
240 artifact,
241 repository,
242 artifact + " failed to transfer from "
243 + repository.getUrl() + " during a previous attempt. This failure"
244 + " was cached in the local repository and"
245 + " resolution is not reattempted until the update interval of " + repository.getId()
246 + " has elapsed or updates are forced. Original error: " + LogSanitizer.sanitize(error),
247 true);
248 }
249 }
250
251 @Override
252 public void checkMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
253 requireNonNull(session, "session cannot be null");
254 requireNonNull(check, "check cannot be null");
255 final String updatePolicy = check.getMetadataPolicy();
256 if (check.getLocalLastUpdated() != 0
257 && !isUpdatedRequired(session, check.getLocalLastUpdated(), updatePolicy)) {
258 LOGGER.debug("Skipped remote request for {} locally installed metadata up-to-date", check.getItem());
259
260 check.setRequired(false);
261 return;
262 }
263
264 Metadata metadata = check.getItem();
265 RemoteRepository repository = check.getRepository();
266
267 Path metadataPath =
268 requireNonNull(check.getPath(), String.format("The metadata '%s' has no file attached", metadata));
269
270 boolean fileExists = check.isFileValid() && Files.exists(metadataPath);
271
272 Path touchPath = getMetadataTouchFile(metadataPath);
273 Properties props = read(touchPath);
274
275 String updateKey = getUpdateKey(session, metadataPath, repository);
276 String dataKey = getDataKey(metadataPath);
277
278 String error = getError(props, dataKey);
279
280 long lastUpdated;
281 if (error == null) {
282 if (fileExists) {
283
284 lastUpdated = getLastUpdated(props, dataKey);
285 } else {
286
287 lastUpdated = TS_NEVER;
288 }
289 } else if (error.isEmpty()) {
290
291 lastUpdated = getLastUpdated(props, dataKey);
292 } else {
293
294 String transferKey = getTransferKey(session, metadataPath, repository);
295 lastUpdated = getLastUpdated(props, transferKey);
296 }
297
298 if (lastUpdated == TS_NEVER) {
299 check.setRequired(true);
300 } else if (isAlreadyUpdated(session, updateKey)) {
301 LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem());
302
303 check.setRequired(false);
304 if (error != null) {
305 check.setException(newException(error, metadata, repository));
306 }
307 } else if (isUpdatedRequired(session, lastUpdated, updatePolicy)) {
308 check.setRequired(true);
309 } else if (fileExists) {
310 LOGGER.debug("Skipped remote request for {}, locally cached metadata up-to-date", check.getItem());
311
312 check.setRequired(false);
313 } else {
314 int errorPolicy = Utils.getPolicy(session, metadata, repository);
315 int cacheFlag = getCacheFlag(error);
316 if ((errorPolicy & cacheFlag) != 0) {
317 check.setRequired(false);
318 check.setException(newException(error, metadata, repository));
319 } else {
320 check.setRequired(true);
321 }
322 }
323 }
324
325 private MetadataTransferException newException(String error, Metadata metadata, RemoteRepository repository) {
326 if (error == null || error.isEmpty()) {
327 return new MetadataNotFoundException(
328 metadata,
329 repository,
330 metadata + " was not found in "
331 + repository.getUrl() + " during a previous attempt."
332 + " This failure was cached in the local repository and"
333 + " resolution is not be reattempted until the update interval of " + repository.getId()
334 + " has elapsed or updates are forced",
335 true);
336 } else {
337 return new MetadataTransferException(
338 metadata,
339 repository,
340 metadata + " failed to transfer from "
341 + repository.getUrl() + " during a previous attempt."
342 + " This failure was cached in the local repository and"
343 + " resolution will not be reattempted until the update interval of " + repository.getId()
344 + " has elapsed or updates are forced. Original error: " + LogSanitizer.sanitize(error),
345 true);
346 }
347 }
348
349 private long getLastUpdated(Properties props, String key) {
350 String value = props.getProperty(key + UPDATED_KEY_SUFFIX, "");
351 try {
352 return (!value.isEmpty()) ? Long.parseLong(value) : TS_UNKNOWN;
353 } catch (NumberFormatException e) {
354 LOGGER.debug("Cannot parse last updated date {}, ignoring it", value, e);
355 return TS_UNKNOWN;
356 }
357 }
358
359 private String getError(Properties props, String key) {
360 return props.getProperty(key + ERROR_KEY_SUFFIX);
361 }
362
363 private Path getArtifactTouchFile(Path artifactPath) {
364 return artifactPath.getParent().resolve(artifactPath.getFileName() + UPDATED_KEY_SUFFIX);
365 }
366
367 private Path getMetadataTouchFile(Path metadataPath) {
368 return metadataPath.getParent().resolve("resolver-status.properties");
369 }
370
371 private String getDataKey(RemoteRepository repository) {
372 Set<String> mirroredUrls = Collections.emptySet();
373 if (repository.isRepositoryManager()) {
374 mirroredUrls = new TreeSet<>();
375 for (RemoteRepository mirroredRepository : repository.getMirroredRepositories()) {
376 mirroredUrls.add(normalizeRepoUrl(mirroredRepository.getUrl()));
377 }
378 }
379
380 StringBuilder buffer = new StringBuilder(1024);
381
382 buffer.append(normalizeRepoUrl(repository.getUrl()));
383 for (String mirroredUrl : mirroredUrls) {
384 buffer.append('+').append(mirroredUrl);
385 }
386
387 return buffer.toString();
388 }
389
390 private String getTransferKey(RepositorySystemSession session, RemoteRepository repository) {
391 return getRepoKey(session, repository);
392 }
393
394 private String getDataKey(Path metadataPath) {
395 return metadataPath.getFileName().toString();
396 }
397
398 private String getTransferKey(RepositorySystemSession session, Path metadataPath, RemoteRepository repository) {
399 return metadataPath.getFileName().toString() + '/' + getRepoKey(session, repository);
400 }
401
402 private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) {
403 StringBuilder buffer = new StringBuilder(128);
404
405 Proxy proxy = repository.getProxy();
406 if (proxy != null) {
407 buffer.append(AuthenticationDigest.forProxy(session, repository)).append('@');
408 buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>');
409 }
410
411 buffer.append(AuthenticationDigest.forRepository(session, repository)).append('@');
412
413 buffer.append(repository.getContentType()).append('-');
414 buffer.append(repository.getId()).append('-');
415 buffer.append(normalizeRepoUrl(repository.getUrl()));
416
417 return buffer.toString();
418 }
419
420 private String normalizeRepoUrl(String url) {
421 String result = url;
422 if (url != null && !url.isEmpty() && !url.endsWith("/")) {
423 result = url + '/';
424 }
425 return result;
426 }
427
428 private String getUpdateKey(RepositorySystemSession session, Path path, RemoteRepository repository) {
429 return path.toAbsolutePath() + "|" + getRepoKey(session, repository);
430 }
431
432 private int getSessionState(RepositorySystemSession session) {
433 String mode = ConfigUtils.getString(session, DEFAULT_SESSION_STATE, CONFIG_PROP_SESSION_STATE);
434 if (Boolean.parseBoolean(mode) || "enabled".equalsIgnoreCase(mode)) {
435
436 return STATE_ENABLED;
437 } else if ("bypass".equalsIgnoreCase(mode)) {
438
439 return STATE_BYPASS;
440 } else {
441
442 return STATE_DISABLED;
443 }
444 }
445
446 private boolean isAlreadyUpdated(RepositorySystemSession session, String updateKey) {
447 if (getSessionState(session) >= STATE_BYPASS) {
448 return false;
449 }
450 Object checkedFiles = session.getData().get(SESSION_CHECKS);
451 if (!(checkedFiles instanceof Map)) {
452 return false;
453 }
454 return ((Map<?, ?>) checkedFiles).containsKey(updateKey);
455 }
456
457 @SuppressWarnings("unchecked")
458 private void setUpdated(RepositorySystemSession session, String updateKey) {
459 if (getSessionState(session) >= STATE_DISABLED) {
460 return;
461 }
462 Object checkedFiles = session.getData().computeIfAbsent(SESSION_CHECKS, () -> new ConcurrentHashMap<>(256));
463 ((Map<String, Boolean>) checkedFiles).put(updateKey, Boolean.TRUE);
464 }
465
466 private String getNotFoundKey(Path touchPath, String dataKey) {
467 return touchPath.toAbsolutePath() + "|" + dataKey;
468 }
469
470
471
472
473
474 @SuppressWarnings("unchecked")
475 private void setNotFound(RepositorySystemSession session, String notFoundKey) {
476 Object notFounds = session.getData().computeIfAbsent(SESSION_NOT_FOUNDS, ConcurrentHashMap::newKeySet);
477 ((Set<String>) notFounds).add(notFoundKey);
478 }
479
480 private boolean isNotFoundInSession(RepositorySystemSession session, String notFoundKey) {
481 Object notFounds = session.getData().get(SESSION_NOT_FOUNDS);
482 return notFounds instanceof Set && ((Set<?>) notFounds).contains(notFoundKey);
483 }
484
485 private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) {
486 return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy);
487 }
488
489 private Properties read(Path touchPath) {
490 Properties props = trackingFileManager.read(touchPath);
491 return (props != null) ? props : new Properties();
492 }
493
494 @Override
495 public void touchArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
496 requireNonNull(session, "session cannot be null");
497 requireNonNull(check, "check cannot be null");
498 Path artifactPath = check.getPath();
499 Path touchPath = getArtifactTouchFile(artifactPath);
500
501 String updateKey = getUpdateKey(session, artifactPath, check.getRepository());
502 String dataKey = getDataKey(check.getAuthoritativeRepository());
503 String transferKey = getTransferKey(session, check.getRepository());
504
505 setUpdated(session, updateKey);
506 Map<String, String> staleSiblingNotFounds = Collections.emptyMap();
507 if (check.getException() == null) {
508 staleSiblingNotFounds = getStaleSiblingNotFounds(
509 session, read(touchPath), touchPath, dataKey, check.getItem(), check.getRepository());
510 } else if (check.getException() instanceof ArtifactNotFoundException) {
511 setNotFound(session, getNotFoundKey(touchPath, dataKey));
512 }
513 Properties props = write(touchPath, dataKey, transferKey, check.getException(), staleSiblingNotFounds);
514
515 if (Files.exists(artifactPath) && !hasErrors(props)) {
516 trackingFileManager.delete(touchPath);
517 }
518 }
519
520
521
522
523
524
525
526
527
528
529 private Map<String, String> getStaleSiblingNotFounds(
530 RepositorySystemSession session,
531 Properties props,
532 Path touchPath,
533 String dataKey,
534 Artifact artifact,
535 RemoteRepository repository) {
536 Map<String, String> removals = null;
537 for (Object k : props.keySet()) {
538 String key = k.toString();
539 if (key.endsWith(ERROR_KEY_SUFFIX)) {
540 String otherDataKey = key.substring(0, key.length() - ERROR_KEY_SUFFIX.length());
541 if (!otherDataKey.equals(dataKey) && NOT_FOUND.equals(props.getProperty(key))) {
542 if (isNotFoundInSession(session, getNotFoundKey(touchPath, otherDataKey))) {
543 LOGGER.debug(
544 "{} was downloaded from {} after {} answered not-found earlier in this session;"
545 + " not caching that not-found",
546 artifact,
547 repository.getUrl(),
548 otherDataKey);
549 } else {
550 LOGGER.warn(
551 "{} was downloaded from {} while a not-found cached by a previous session suppresses"
552 + " re-checking {}; expiring the cached not-found so that repository is"
553 + " checked again on the next resolution",
554 artifact,
555 repository.getUrl(),
556 otherDataKey);
557 }
558 if (removals == null) {
559 removals = new HashMap<>();
560 }
561 removals.put(key, null);
562 removals.put(otherDataKey + UPDATED_KEY_SUFFIX, null);
563 }
564 }
565 }
566 return removals != null ? removals : Collections.emptyMap();
567 }
568
569 private boolean hasErrors(Properties props) {
570 for (Object key : props.keySet()) {
571 if (key.toString().endsWith(ERROR_KEY_SUFFIX)) {
572 return true;
573 }
574 }
575 return false;
576 }
577
578 @Override
579 public void touchMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
580 requireNonNull(session, "session cannot be null");
581 requireNonNull(check, "check cannot be null");
582 Path metadataPath = check.getPath();
583 Path touchPath = getMetadataTouchFile(metadataPath);
584
585 String updateKey = getUpdateKey(session, metadataPath, check.getRepository());
586 String dataKey = getDataKey(metadataPath);
587 String transferKey = getTransferKey(session, metadataPath, check.getRepository());
588
589 setUpdated(session, updateKey);
590 write(touchPath, dataKey, transferKey, check.getException(), Collections.emptyMap());
591 }
592
593 private Properties write(
594 Path touchPath, String dataKey, String transferKey, Exception error, Map<String, String> extraUpdates) {
595 Map<String, String> updates = new HashMap<>(extraUpdates);
596
597 String timestamp = Long.toString(System.currentTimeMillis());
598
599 if (error == null) {
600 updates.put(dataKey + ERROR_KEY_SUFFIX, null);
601 updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp);
602 updates.put(transferKey + UPDATED_KEY_SUFFIX, null);
603 } else if (error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException) {
604 updates.put(dataKey + ERROR_KEY_SUFFIX, NOT_FOUND);
605 updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp);
606 updates.put(transferKey + UPDATED_KEY_SUFFIX, null);
607 } else {
608 String msg = error.getMessage();
609 if (msg == null || msg.isEmpty()) {
610 msg = error.getClass().getSimpleName();
611 }
612
613
614 updates.put(dataKey + ERROR_KEY_SUFFIX, LogSanitizer.sanitize(msg));
615 updates.put(dataKey + UPDATED_KEY_SUFFIX, null);
616 updates.put(transferKey + UPDATED_KEY_SUFFIX, timestamp);
617 }
618
619 return trackingFileManager.update(touchPath, updates);
620 }
621 }